/* Layout primitives: the grouped-list language the app uses, in CSS.
 *
 * "Glass at the edge, opaque inside" — a group is a padded panel; the rows
 * inside it are the family fill, edge to edge, separated by hairlines. The
 * panel's padding is what leaves something for a translucent surface to
 * refract, so it is not decoration and must not be trimmed away.
 */

*, *::before, *::after { box-sizing: border-box; }

/* ── `hidden` HAS TO WIN, AND IT DOES NOT ON ITS OWN ─────────────────────────
 *
 * The browser hides a [hidden] element from its OWN stylesheet, and an author
 * stylesheet beats a user-agent one at any specificity — so every rule here that
 * sets `display` silently un-hides whatever it matches. `.btn { display:
 * inline-flex }` is the one that already cost us: page-settings.html renders its
 * browser-push Enable button `hidden` and the script un-hides it when a push
 * subscription is possible, and the button was visible the whole time.
 *
 * The failure is completely silent — correct markup, correct script, a control
 * that simply ignores being hidden — and it comes back for every new component
 * with a display rule and a hidden state. The photo viewer's arrows are the
 * second: they are `inline-flex` and are hidden for a run of one picture.
 *
 * !important is what the rule needs to be worth having. Toggling `el.hidden` is
 * the plain way to show and hide something in this codebase and it has to mean
 * what it says; anything that genuinely needs to override it can set the
 * attribute rather than fight the cascade. */
[hidden] { display: none !important; }

/* ── THE TYPE SCALE ─────────────────────────────────────────────────────────
 *
 * # WHY THE WHOLE PAGE GOT BIGGER, AND WHY IT IS NOT JUST TASTE
 *
 * The old base was 15px/1.45, which is a PHONE size: it comes from the app,
 * where the reader holds the screen 30cm from their face and every row is a
 * touch target. A desktop browser is roughly twice that distance away, and the
 * same 15px there is small enough that people lean in — which is the tell.
 *
 * 16px/1.5 is the browser's own default for a reason, and the six steps below
 * are the whole scale. SIX, NOT THIRTY: before this there were a dozen loose
 * font-size declarations between 12px and 26px, chosen a rule at a time, and
 * the difference between 13 and 14 in two adjacent components was never a
 * decision anybody made. Naming the steps is what stops the next one drifting.
 *
 * REMS, NOT PIXELS, for everything on the scale. A person who has set a larger
 * default text size in their browser has asked for larger text, and a pixel
 * value refuses them silently. Pixels stay for things that are NOT text —
 * hairlines, radii, touch targets, the rail widths.
 *
 * The 44px touch targets are untouched. They are a hit-area minimum and have
 * nothing to do with how big the words inside them are.
 *
 * The micro-labels that are still bare 12px — the unread badge, the chips on a
 * card's edge, the ad slot's disclosure — are deliberately off the scale. They
 * are not text somebody reads, they are marks somebody counts, and scaling them
 * with the body would push them off the edges they hang on.
 *
 * ── AND THERE ARE TWO TIERS NOW (2026-09-06) ───────────────────────────────
 *
 * The reasoning above stopped one step short of its own conclusion. If a
 * desktop browser is twice as far away as a phone, then ONE scale cannot be
 * right for both — 16px was chosen as a compromise between the two distances
 * and, like most compromises between distances, it is slightly wrong at each.
 *
 * So the base below is the PHONE scale and the block after it is the desktop
 * one, at the same 701px where the nav stops being a bar and the sign-in panel
 * stops being a sheet. Nothing in the sheet reads the numbers directly; every
 * rule on the site already goes through the six tokens, which is what makes a
 * second tier three lines of CSS instead of an audit.
 *
 * THE CAPTION MOVES PROPORTIONALLY LESS THAN THE DISPLAY, and that is the
 * point of scaling a ramp rather than multiplying a base. A timestamp is
 * ambient at any distance; a large title has to carry a page that is now
 * twice as wide. Scaling everything by one factor keeps a hierarchy that was tuned for
 * a 390px column and stretches it across 1200px, where it reads as flat.
 */
:root {
  --type-caption: 0.8125rem;  /* 13px — LiquidType.footnote: timestamps, handles, the second line */
  --type-small: 0.9375rem;    /* 15px — LiquidType.itemBody / secondary */
  --type-body: 1.0625rem;     /* 17px — LiquidType.body: the base, and every post body */
  --type-title: 1.0625rem;    /* 17px — LiquidType.title (set 600): a name, a row title */
  --type-heading: 1.375rem;   /* 22px — LiquidType.title2 */
  /* 30px. It was the landing hero's, and the hero is gone (2026-09-06) —
   * this is now the page head's large title, which is the app's large title
   * and was carrying a hardcoded 30px of its own. One reader, and the right
   * one: a token whose only consumer is deleted is a token that drifts. */
  --type-display: 2.125rem;   /* 34px — LiquidType.largeTitle */

  /* ── THE RAIL WIDTH ──────────────────────────────────────────────────────
   *
   * A token because it is used in two places that MUST agree: the width of the
   * fixed rail, and the left padding that holds the page clear of it. They are
   * not two decisions, they are one number read twice, and the failure when
   * they drift is content sliding under the navigation — which looks like a
   * z-index problem and is not.
   *
   * PIXELS, NOT REMS, unlike everything above. This is a column of icons, not
   * a column of text: at a larger browser default the labels inside it should
   * grow and the chrome around them should not, or the rail eats the page for
   * exactly the reader who has least screen to spare.
   *
   * Redefined at each breakpoint below. 244 fits the longest label in the
   * languages we ship; 76 is an icon plus the whitespace that stops a column of
   * them reading as a toolbar; 60 is the floor, where the content column on a
   * 360px phone still gets 300. */
  --nav-w: 72px;
}

/* THE DESKTOP TIER. See "AND THERE ARE TWO TIERS NOW" above for why one scale
 * cannot serve a screen held at 30cm and a screen sat at 60.
 *
 * 701px, which is the site's one breakpoint and not a number chosen for type.
 * A page whose text grew at a width its navigation did not would be two
 * designs meeting in the middle, and the reader would feel the seam without
 * being able to name it.
 *
 * --nav-w is set with the rail's own rules (232px above 700px), not here. */
/* ── RETIRED 2026-09-21 — the desktop type tier ─────────────────────────────
 *
 *   @media (min-width: 701px) { :root { caption 14, small 16, body 17,
 *     title 20, heading 24, display 40 } }
 *
 * The argument above is sound for a reading site and wrong for this one: the
 * web is the app on a bigger screen, and a 20px row title and 24px section
 * header beside the app's 17 and 13 read as a different, louder product. The
 * scale is the app's LiquidType at every width now (lib/core/type.dart), in
 * Outfit, the app's face. The column is 640px either way, so line length is
 * unchanged. */

/* OUTFIT, THE APP'S FACE (pubspec.yaml). The same four TTFs the app bundles,
 * served beside this sheet from web/fonts/ on the assets host — which is why
 * font-src names that origin (lib-middleware.go). swap, so a slow font never
 * hides text; the fallback stack is the system face the site used before. */
@font-face { font-family: "Outfit"; src: url("fonts/Outfit-Regular.ttf") format("truetype"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Outfit"; src: url("fonts/Outfit-Medium.ttf") format("truetype"); font-weight: 500; font-display: swap; }
@font-face { font-family: "Outfit"; src: url("fonts/Outfit-SemiBold.ttf") format("truetype"); font-weight: 600; font-display: swap; }
@font-face { font-family: "Outfit"; src: url("fonts/Outfit-Bold.ttf") format("truetype"); font-weight: 700; font-display: swap; }

body {
  margin: 0;
  background: var(--liquid-grouped-background);
  color: var(--liquid-primary-label);
  font: var(--type-body)/1.4 "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* A handle or a URL with no break point wraps rather than widening the page. */
  overflow-wrap: break-word;
}

/* THE APP'S TOUCH, NOT A BROWSER'S (2026-10-03, user asked). Three browser
 * behaviours the app has no counterpart for, off on every page of this layout:
 *
 * 1. NO TAP FLASH. A phone browser paints a square over whatever a finger
 *    taps — a bar tab, the ＋, a chip. Every control here answers its own
 *    press already, so the square was a second, square-cornered answer drawn
 *    round a round control.
 * 2. CHROME IS NOT TEXT. A sheet title, a label or a button could be dragged
 *    over and selected; in the app none of them can. What the app DOES let you
 *    select is a message — a ping's or an update's (SelectionArea in
 *    ping_blocks_view.dart) — which is .blocks here, and fields stay
 *    editable. A message inside a card that is a link stays part of the card,
 *    where a hold opens the card's menu.
 * 3. NO LINK CALLOUT ON A LONG PRESS. This is iOS's half; Android reports the
 *    same press as a contextmenu event, refused for touch in layout.html. */
.app {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.app :is(input, textarea, select, [contenteditable]),
.app .blocks {
  -webkit-user-select: text;
  user-select: text;
}
.app :is(a, [data-card-menu]) .blocks {
  -webkit-user-select: none;
  user-select: none;
}

/* Form controls take the page face; a browser gives them its own otherwise. */
button, input, select, textarea { font-family: inherit; }
/* The app's ticks and radios are the brand colour, not the browser's blue. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--liquid-primary-action); width: 20px; height: 20px; }

a { color: var(--liquid-accent); }

/* The page column. Wide enough for a list, never wide enough that a line of
 * text becomes hard to track back to the next one.
 *
 * IT GOT NARROWER AS THE TYPE GOT BIGGER, which is the same decision stated
 * twice: readable line length is measured in CHARACTERS — roughly 60 to 75 of
 * them — not in pixels, so a column that was right at 15px is too wide at 16px.
 * 640px at this size is about 70 characters.
 *
 * margin:auto still centres it when there is no right rail, which is every
 * signed-out page and every width below the rail breakpoint. */
.page {
  max-width: 640px;
  margin: 0 auto;
  padding: 20px 16px;
}

/* ── THE LANGUAGE ROW (2026-10-13) ───────────────────────────────────────────
 *
 * The last thing on a signed-out page: every language in its own name, the
 * current one in the label colour. See partial-languages.html. Each item is
 * padded to a 32px tap height; the row wraps rather than scrolls.
 */
.langrow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 16px;
  margin: 32px 0 0;
  padding: 12px 0 0;
  border-top: 0.5px solid var(--liquid-separator);
  font-size: 13px;
  color: var(--liquid-secondary-label);
}
.langrow__item {
  display: inline-block;
  padding: 7px 0;
  color: inherit;
  text-decoration: none;
}
a.langrow__item:hover { text-decoration: underline; }
.langrow__item--current { color: var(--liquid-primary-label); font-weight: 600; }

/* ── RETIRED — .appbar, .appbar__title, .appbar__links, .appbar__signout ─────
 *
 * A `position: sticky` band across the top of the page: the page title on the
 * left, and Profile / Settings / sign-out (or both auth doors) on the right. It
 * was drawn below 700px for a member and at EVERY width for a visitor, who had
 * no rail at all.
 *
 * It went for three reasons, in order of weight:
 *
 * 1. THE APP HAS NOTHING LIKE IT. The app draws a LARGE title that collapses as
 *    the list scrolls, with the account as small round chips on the trailing
 *    edge (LiquidCollapsingAppBar, home_screen.dart). A permanent solid band is
 *    a shape this product does not use anywhere, so the one screen most people
 *    see first looked least like the thing it is a website for.
 * 2. IT COST HEIGHT ON EVERY SCREEN OF EVERY SCROLL. Pinned chrome is paid for
 *    once per screenful; chrome that scrolls away is paid for once. On a phone
 *    reading a timeline that is the difference between five cards and four.
 * 3. IT EXISTED FOR THE VISITOR'S DOORS. Sign In and Sign Up had nowhere else
 *    to go, because a signed-out page had no rail. It has one now — see the
 *    public branch in partial-nav.html — so the bar was carrying one thing that
 *    is now carried better somewhere else.
 *
 * .pagehead below is the replacement and is NOT the same element renamed: it is
 * in the scrolling column, it is phone-only, and it holds two chips rather than
 * three controls. Nothing is display:none'd into place for the desktop.
 */

/* ── THE PAGE HEAD: THE APP'S LARGE TITLE, ON A PHONE ────────────────────────
 *
 * A large title and the account chips beside it, at the top of the content
 * column and scrolling with it.
 *
 * PHONE ONLY. Above 700px the rail names the section, highlights it, and
 * carries Profile and Settings at its foot, so a heading repeating the section
 * name would be the same word twice on one screen — the decision the old bar
 * already made with `.app .appbar { display: none }` and the one thing about it
 * that was right. <title> names the page in the tab at every width.
 *
 * `display: none` here rather than `display: flex` inside the phone query,
 * because the header is in the markup on every page and the desktop must not
 * pay a frame of it before the query applies. */
/* AT EVERY WIDTH since 2026-09-21: the app opens every screen with a large
 * title, and the desktop was the one place a page had no name on it. The
 * account chips stay phone-only — the right rail carries them above 700px. */
@media all {
  /* A GRID OF THREE, THE TITLE CENTRED (2026-09-24): the app bar centres its
   * title between the back chevron and the trailing buttons, and so does this.
   * The two side columns share the free space equally, so the title sits in
   * the middle of the column unless one side's buttons outgrow their half; a long title
   * scrolls inside the middle column rather than pushing a side off. */
  .pagehead {
    display: grid;
    grid-template-columns: minmax(max-content, 1fr) minmax(0, auto) minmax(max-content, 1fr);
    align-items: start;
    gap: 12px;
    /* Air ABOVE it, not below: the title is the first thing in the column and
     * needs to clear the browser's own chrome; what follows it is a group or a
     * card that brings its own margin. */
    margin: 4px 0 14px;
  }

  /* THE LARGE TITLE. 30px/700 with the tracking tightened, which is what the
   * app's large title is — not the 17px semibold the old bar used. A large
   * title is a landmark you scroll past once; a bar title is a label that has
   * to stay small because it is permanent. Different jobs, different sizes.
   *
   * ONE LINE, SCROLLED SIDEWAYS WHEN IT IS LONG (2026-09-26, user asked). It
   * wrapped until then, and on a phone "Brazil Trending Videos" became three
   * lines of 30px — a third of the screen spent on a heading. An ellipsis would
   * cut off the end for good; this keeps all of it, one swipe away. The
   * scrollbar is hidden, and an edge that has more text beyond it fades out
   * (below), so the reader can see there is more to read. */
  .pagehead__title {
    margin: 0;
    font-size: var(--type-display);
    font-weight: 700;
    letter-spacing: -0.2px;
    line-height: 1.18;
    min-width: 0;
    text-align: center;
    white-space: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .pagehead__title::-webkit-scrollbar { display: none; }

  /* THE FADE SAYS "MORE THIS WAY". A scroll-driven animation on the title's
   * own inline scroll: at the start only the trailing edge fades, at the end
   * only the leading one, in between both. A title that fits has no scroll
   * range, so the timeline is inactive and nothing is masked — no script
   * measuring widths. A browser without scroll timelines gets a hard edge,
   * which still scrolls. */
  @supports (animation-timeline: scroll()) {
    .pagehead__title {
      --pagehead-fade-l: 0px;
      --pagehead-fade-r: 0px;
      animation: pagehead-title-fade linear both;
      animation-timeline: scroll(self inline);
      -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--pagehead-fade-l), #000 calc(100% - var(--pagehead-fade-r)), transparent);
      mask-image: linear-gradient(to right, transparent, #000 var(--pagehead-fade-l), #000 calc(100% - var(--pagehead-fade-r)), transparent);
    }
  }

  .pagehead__lead { display: flex; justify-content: flex-start; min-width: 0; }
  .pagehead__trail {
    display: flex;
    justify-content: flex-end;
    align-items: flex-start;
    gap: 8px;
    min-width: 0;
  }

  /* THE BACK CHEVRON, ON THE TITLE'S FIRST LINE. Same alignment as the
   * trailing chips and for the same reason: a two-line title must not push the
   * control that leads it into the middle of the block.
   *
   * A NEGATIVE INSET so the glyph, rather than the disc around it, lines up
   * with the text below. The chip is a 36px circle and the chevron sits about
   * 8px inside it, which read as an indent nobody asked for. */
  .pagehead__back { flex: 0 0 auto; margin-left: -8px; margin-top: 3px; }

  /* The trailing chips sit on the title's FIRST line, not centred against a
   * block that may be two lines tall. */
  .pagehead__actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    padding-top: 3px;
  }
}
/* Registered so the two fade widths interpolate; an unregistered custom
 * property would flip at the halfway point instead of sliding. */
@property --pagehead-fade-l { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --pagehead-fade-r { syntax: "<length>"; inherits: false; initial-value: 0px; }
@keyframes pagehead-title-fade {
  0%   { --pagehead-fade-l: 0px;  --pagehead-fade-r: 28px; }
  8%   { --pagehead-fade-l: 28px; }
  92%  { --pagehead-fade-r: 28px; }
  100% { --pagehead-fade-l: 28px; --pagehead-fade-r: 0px; }
}
@media (min-width: 701px) {
  .pagehead { margin: 12px 0 24px; }
  .pagehead__actions { display: none; }
}

/* A ROUND CHIP: one glyph, 36px, in the flat chrome fill with the content
 * hairline. The app's trailing app-bar chips, which are the same circle in
 * glass. 36 is under the 44px touch floor as a BOX and not as a target — the
 * gap beside it belongs to nothing else, so the pressable area is 44 wide. */
.chipbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-flat-chrome-fill);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-primary-label);
  text-decoration: none;
}
.chipbtn:hover { background: var(--liquid-fill-quiet); }
.chipbtn .nav__glyph { width: 20px; height: 20px; }

/* THE AVATAR CHIP IS FILLED, and it is the one place the accent is allowed in
 * the chrome — because it is not chrome, it is you. (The right rail's identity
 * avatar, .rail__avatar, took the same fill until that rail was retired on
 * 2026-09-24.) */
.chipbtn--me {
  position: relative;
  overflow: hidden;
  background: var(--liquid-accent);
  border-color: transparent;
  color: #fff;
}
.chipbtn--me:hover { background: var(--liquid-accent); }
/* THE PHOTO COVERS THE DISC when there is one (2026-09-26). A Google account's
 * default picture is a letter on its own colour; drawing our initial on the
 * accent instead threw that colour away, so the chip did not show the person
 * the app shows in the same corner. The initial stays underneath for a photo
 * that fails to load. */
.chipbtn__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.chipbtn__initial { font-size: 15px; font-weight: 700; line-height: 1; }

/* ON A PHONE, THE APP BAR AT THE APP'S SIZES (2026-10-03, user asked) —
 * LiquidAppBar: a 44 row with 16 either side of it, the title 17 semibold
 * (the large title is a desktop heading), and every chip 44 with a 24 glyph
 * box — the chevron's path sits inside its box at about the size of the
 * app's — and AppBarAvatarChip a 38 avatar inside a ring of chrome. The
 * page's gutter is the app's 20, so the head reaches 4 past it to land on 16. */
@media (max-width: 700px) {
  .pagehead {
    align-items: center;
    min-height: 44px;
    margin: 0 -4px 12px;
  }
  .pagehead__title {
    font-size: 17px;
    font-weight: 600;
    line-height: 44px;
  }
  .pagehead__trail { align-items: center; }
  .pagehead__back { margin: 0; }
  .pagehead__actions { padding-top: 0; }
  .chipbtn { width: 44px; height: 44px; }
  .chipbtn .nav__glyph { width: 24px; height: 24px; }
  .chipbtn--me {
    border-color: var(--liquid-content-border);
    box-shadow: inset 0 0 0 2px var(--liquid-flat-chrome-fill);
  }
  .chipbtn--me .chipbtn__img {
    top: 2px;
    left: 2px;
    width: calc(100% - 4px);
    height: calc(100% - 4px);
  }
  .chipbtn__initial { font-size: 17px; }
}

/* A capsule at chip height, for the doors in the page head. The full 44px
 * button beside a 36px circle makes the row look like two different toolbars. */
.btn--compact {
  min-height: 32px;
  padding: 0 14px;
  font-size: 15px;
}

/* A grouped list: the panel. */
/* THE OUTLINE IS NOT DECORATION. Every LiquidContentSurface in the app draws a
 * fill and a hairline; the web copied the fill and left the hairline out, and
 * the fill alone is #F1F5F9 on a #F2F2F7 page — four points of lightness. The
 * result was a site of panels nobody could see the edges of, which reads as an
 * unstyled document rather than as a design choice. See --liquid-content-border
 * in tokens.css, which is a different value from --liquid-separator on purpose:
 * that one divides rows INSIDE a surface, this one is the surface's own edge.
 *
 * 1px, not var(--liquid-hairline). A half-pixel border is a half-pixel of paint
 * on a 2x screen and frequently NOTHING at 1x, where the browser rounds it
 * away — which is the one device class where the fill difference is also least
 * visible. The app draws these at width 1. */
.group {
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-group);
  overflow: hidden;
  margin: 0 0 24px;
}
/* A GROUP OPENED BY ADDRESS (2026-10-13) — the person page's Get pings from
 * a circle's "Delivered where": clear of the app bar, and one pulse of the
 * accent round it, as the app's focusGetPings. */
.group--focusable { scroll-margin-top: 72px; }
.group--focusable:target { animation: group-focus 1.6s ease-out 1; }
@keyframes group-focus {
  0%, 30% { box-shadow: 0 0 0 3px var(--liquid-accent); }
  100% { box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .group--focusable:target { animation: none; box-shadow: 0 0 0 2px var(--liquid-accent); }
}

/* A SECTION HEADER IS THE APP'S AGAIN — 2026-09-21.
 *
 * It had become a 24px bold heading inside the panel, on the argument that a
 * desktop page holding three groups needs something for the eye to land on.
 * The user's ruling is that the web looks like the app, and the app's
 * LiquidSectionHeader is 13px medium, uppercase, 0.4 tracking, secondary
 * colour, sitting ABOVE the surface with 8px under it. The templates moved the
 * <h2> out of the <section class="group"> to match — a header inside a white
 * card and one above it are different shapes, and CSS cannot turn one into the
 * other. The large title at the top of the page is what the eye lands on now,
 * as it is in the app. */
.group__header {
  font-size: var(--type-caption);
  font-weight: 500;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--liquid-secondary-label);
  margin: 0;
  padding: 0 4px 8px;
}
/* A header still INSIDE a panel (a form's hidden input came first, or a
 * nested fieldset) keeps the row inset so it does not touch the corner. */
.group > .group__header,
.group > form > .group__header { padding: 14px 16px 6px; }

/* A HEADER WITH AN ACTION BESIDE IT — the "See all" pattern.
 *
 * The heading keeps being a heading and the action keeps being a link, which is
 * the point: a clickable <h2> is undiscoverable, and there is no shape that
 * makes it discoverable without giving a heading the appearance of a control.
 *
 * align-items:baseline, not center. The two have very different font sizes and
 * centring them puts the small one visually high — the misalignment is subtle
 * enough that it reads as sloppiness rather than as a bug. */
.group__headrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 0 4px 8px;
}

/* The heading inside that row has already been padded by the row. */
.group__header--flush { padding: 0; }

/* A HEADING WITH NO PANEL UNDER IT.
 *
 * The channel post list on a channel's own page lost its .group wrapper — see the note in
 * page-channel-detail.html — and the heading stayed, because it is the one line
 * saying the list below is the channel's pings rather than more of its settings.
 *
 * The base rule's padding exists to hold a bold line off a rounded top corner
 * that is no longer there, so the horizontal inset goes and the vertical one
 * becomes the gap above the first card. Left flush with the page gutter, which
 * is where every card edge below it starts. */
.group__header--bare { padding: 0 4px 8px; }

.group__action {
  flex: 0 0 auto;
  font-size: var(--type-caption);
  font-weight: 500;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--liquid-primary-action);
  text-decoration: none;
}
button.group__action { background: none; border: 0; padding: 0; font-family: inherit; line-height: inherit; cursor: pointer; }
.group__headrow > form { display: contents; }

/* A row. Hairlines between, none after the last — the ::after trick keeps that
 * true without a class on the final element, which matters when the list is
 * rendered by a range and the last item is not knowable in the template. */
.row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  min-height: 56px; /* the iOS touch target, and a sane click target here */
  color: inherit;
  text-decoration: none;
}

/* THE HAIRLINE BETWEEN TWO ROWS, and it is drawn by the SECOND of the pair
 * rather than by the first — so the last row in a panel has nothing under it
 * without anybody having to know which one is last.
 *
 * # IT WAS FOUR SELECTORS, AND IT IS ONE AGAIN — 2026-09-20
 *
 * A row carrying a ⋯ menu was wrapped in .rowwrap, because the menu has to be
 * the anchor's SIBLING rather than its child (a <details> inside an <a> is
 * invalid HTML the browser repairs by closing the anchor early). That made two
 * consecutive rows non-adjacent, `.row + .row` stopped matching, and a panel of
 * rows rendered with no dividers at all — so three more combinations were
 * spelled out beside it.
 *
 * .rowwrap had exactly one user left, page-channels-manage.html, and that page
 * is retired: its two lists are the Following and Publishing segments of
 * /channels. Nothing on the site wraps a ROW any more — .cardwrap and
 * .acardwrap still wrap a card and a ping card, and they are untouched — so
 * the three extra selectors matched nothing and are gone with it.
 *
 * IF A ROW MENU COMES BACK, so do they. The wrapper's own rules are retired
 * below with the same note, and this is the pairing that has to be restored
 * with them: a list that mixes wrapped and plain rows needs all four, and the
 * symptom of forgetting is a panel with no hairlines, which reads as a
 * rendering fault rather than as a design. */
.row + .row::before {
  content: "";
  position: absolute;
  top: 0;
  left: 16px;
  right: 0;
  height: 1px;
  background: var(--liquid-separator);
}
/* LiquidListSection indents its dividers 56 when the rows carry a glyph chip. */
.row:has(> .row__icon) + .row::before { left: 56px; }

/* THE GLYPH CHIP — LiquidListTile, 2026-09-21: 36 square, radius 10, the tint at
 * 12% behind an 18pt glyph. --tint defaults to the secondary label, as
 * iconColor does in the app; the modifiers are the palette colours the app's
 * own tiles pass. */
.row__icon {
  --tint: var(--liquid-secondary-label);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  color: var(--tint);
  background: color-mix(in srgb, var(--tint) 12%, transparent);
}
.row__icon .nav__glyph { width: 18px; height: 18px; }
.row__icon--ping   { --tint: #5374CC; }
/* "(You)" after the reader's own name in FROM (2026-10-07): regular, lighter. */
.row__titletail { font-weight: 400; color: var(--liquid-secondary-label); }
/* A send that never went out, on the Timeline: its time in the secondary
 * label (FiringFacts.muted). */
.row__title--muted { color: var(--liquid-secondary-label); }
.row__icon--brand   { --tint: #0C8863; }
.row__icon--people  { --tint: #11829E; }
.row__icon--caution { --tint: #9E6F00; }
.row__icon--channel { --tint: var(--liquid-channel-accent); }
.row__icon--urgent  { --tint: var(--liquid-destructive-icon); }
/* RETIRED 2026-09-28 — .row__icon--info (Palette.info is gone; the one
 * reader, "Allow people to find me", is .row__icon--people now) and
 * .row__icon--push (a delivery method has no colour; the row is grey). */

/* THE DISCLOSURE CHEVRON on every row that is a link, drawn in CSS so no
 * template has to remember it: LiquidSize.disclosure in the tertiary label.
 * A row that already carries a trailing control keeps that instead, the way
 * trailing ??= chevron works in the app. */
a.row::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin: 0 4px 0 2px;
  border-top: 2px solid var(--liquid-tertiary-label);
  border-right: 2px solid var(--liquid-tertiary-label);
  border-radius: 1px;
  transform: rotate(45deg);
}
a.row:has(.row__trailing, .badge, .row__value)::after { display: none; }
a.row:hover { background: var(--liquid-fill-quiet); }

.row__text { flex: 1; min-width: 0; }
.row__title { display: block; font-size: var(--type-title); }
.row__subtitle {
  display: block;
  margin-top: 2px;
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
}

/* A row that is a link. .row already sets `color: inherit` and removes the
 * underline, so all this adds is the hover — and the hover is not decoration:
 * a list where some rows open something and others do not needs to say which is
 * which before the click, not after. */
.row--link:hover { background: var(--liquid-fill-quiet); }

/* THE TEXT COLUMN AS THE LINK, for a row that also carries a verb.
 *
 * A <form> and a <details> are both invalid inside an <a>, and a browser
 * repairs that by closing the anchor early — leaving a row whose bottom half
 * is silently not the link its top half is. So on every row with a button on
 * it (People's requests, Blocked & Declined, both connection lists) the anchor
 * wraps the name and the verb sits beside it as a sibling.
 *
 * It has to restate what .row gives a whole-row link, because here the <a> is
 * the CHILD rather than the row itself. */
.row__text--link {
  color: inherit;
  text-decoration: none;
  border-radius: var(--liquid-radius-row);
}
.row__text--link:hover .row__title { text-decoration: underline; }

.row--destructive { color: var(--liquid-destructive-label); }
.row--destructive .row__title { color: var(--liquid-destructive-icon); }

/* Buttons. The capsule is the verb. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 20px;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-primary-action);
  color: #fff;
  font: inherit;
  font-weight: 600;
  font-size: 17px;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
}

.btn--quiet {
  background: var(--liquid-flat-chrome-fill);
  color: var(--liquid-primary-label);
  border: 1px solid var(--liquid-content-border);
}

.btn--destructive { background: var(--liquid-destructive); color: #fff; }

.btn:disabled { opacity: 0.5; cursor: default; }

/* Reduced motion is honoured wherever motion is added later. Declared now so
 * the first animation added does not have to remember. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── THE htmx INDICATOR, OWNED HERE RATHER THAN INJECTED ────────────────────
 *
 * htmx normally appends these three rules itself, in a <style> element it
 * builds in script. That element carries no nonce and cannot be given one, so
 * under this site's `style-src 'nonce-…' <cdn>` the browser discarded it and
 * logged a violation on every page load. The injection is switched off at the
 * source — see the htmx-config meta tag in layout.html, which explains why the
 * flag was preferred to widening the policy.
 *
 * NOTHING WEARS THESE CLASSES TODAY. They are here so that the day somebody
 * adds an hx-indicator the attribute simply works, instead of failing in the
 * quiet way a missing stylesheet always does. Keep them identical to htmx's
 * own defaults: a spinner that behaves differently from the documented one is
 * worse than no spinner.
 *
 * Both selectors are needed. The first is the indicator INSIDE the requesting
 * element; the second is the requesting element that IS the indicator.
 */
.htmx-indicator { opacity: 0; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
  transition: opacity 200ms ease-in;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* A SIGN-OUT FORM LAID OUT AS A ROW inside a grouped list. The route is
 * POST-only — see the note in partial-nav.html — so every copy of this control
 * on the site is a form, and a form wrapper inside a .group would otherwise
 * introduce a block box that breaks the row rhythm around it. */
.row__form { display: contents; }

/* A ROW THAT IS A <button>. Everything a browser puts on one by default has to
 * come off, or a control that looked like the rows above it would sit two
 * pixels higher, centred, in the system font — the kind of difference nobody
 * can name and everybody sees. Same reset the rail's sign-out row takes. */
.row--button {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* ── THE SHELL: AN ICON RAIL ON A DESKTOP, THE APP'S BAR ON A PHONE ─────────
 *
 * Sections on the left, content in the middle, identity and discovery on the
 * right. The content column holds its 640px and both rails are fixed; what a
 * wider window adds is space around the whole shell, never a longer line.
 *
 * # THE RAIL IS ICON + LABEL ROWS AT EVERY DESKTOP WIDTH — 2026-09-24
 *
 * It was a 244px column above 1264px that drew every sub-screen and every
 * channel as a row, with a scrollbar once the list outgrew the window. On
 * 2026-09-21 it became 72px of icon-only buttons with the label as a hover
 * tooltip; on 2026-09-24 it widened to 232px so each button shows its label
 * beside the icon (see THE WIDE RAIL below). It still draws only the fixed
 * buttons — everything deeper opens in a flyout beside the rail, unchanged —
 * see the header of partial-nav.html. Nothing in the rail scrolls, so
 * it is NOT a scroll container, and that is load-bearing: a scroll container
 * clips on both axes, and the flyouts hang outside it.
 *
 * RETIRED with it: the 244px tier, the 76px middle tier, .nav__sub,
 * .nav__item--child/--grand, .nav__createitem/--group, .nav__createtitle and
 * the badge-in-a-row layout. The history is in git.
 */
.shell {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 32px;
  padding-left: var(--nav-w);
}

@media (min-width: 701px) {
  :root { --nav-w: 232px; }
}

.nav {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 20;
  width: var(--nav-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 16px 0;
  background: var(--liquid-flat-chrome-fill);
  border-right: var(--liquid-hairline) solid var(--liquid-separator);
}
.nav__rows,
.nav__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.nav__foot { margin-top: auto; }

.nav__fly { position: relative; }
.nav__fly > summary { list-style: none; }
.nav__fly > summary::-webkit-details-marker { display: none; }

/* A BUTTON, 52 SQUARE. Grey at rest and the label colour when lit, with the
 * quiet fill behind it — the app's nav bar, not its domain accents: the bar is
 * the one place the app keeps neutral. */
.nav__item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 0;
  border-radius: 14px;
  background: none;
  color: var(--liquid-secondary-label);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.nav__item:hover,
.nav__fly[open] > .nav__item { background: var(--liquid-fill-quiet); color: var(--liquid-primary-label); }
.nav__item--active { background: var(--liquid-fill-quiet); color: var(--liquid-primary-label); }
.nav__item:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }

.nav__glyph {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  stroke-width: 1.7;
}
.nav__item--active .nav__glyph { stroke-width: 2.2; }

/* The disclosure mark, tucked against the button's trailing edge so the glyph
 * stays centred. */
.nav__chev {
  position: absolute;
  right: 3px;
  top: 50%;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  opacity: 0.55;
}
.nav__fly[open] > .nav__item .nav__chev { opacity: 1; }

/* SUPERSEDED 2026-09-24: both the wide desktop rail (THE WIDE RAIL, below) and
 * the phone bar draw the label in place, so this tooltip form no longer shows
 * at any width. Kept as the base the two overrides start from.
 *
 * THE LABEL IS A TOOLTIP on the desktop: invisible until hover or focus, then
 * drawn beside the button. It stays in the markup either way, so a screen
 * reader names every button. Suppressed while that button's flyout is open,
 * where the flyout's own title says the same word. */
.nav__label {
  position: absolute;
  left: calc(100% + 12px);
  top: 50%;
  transform: translateY(-50%);
  padding: 5px 10px;
  border-radius: 8px;
  background: var(--liquid-primary-label);
  color: var(--liquid-grouped-background);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease 250ms;
  z-index: 2;
}
@media (hover: hover) {
  .nav__item:hover .nav__label { opacity: 1; }
}
.nav__item:focus-visible .nav__label { opacity: 1; transition-delay: 0ms; }
.nav__fly[open] > .nav__item .nav__label { opacity: 0; }

/* The create button is the verb, so it is filled — the app's ＋ capsule. */
.nav__item--create {
  margin-bottom: 12px;
  background: var(--liquid-primary-action);
  color: #fff;
}
.nav__item--create:hover,
.nav__fly[open] > .nav__item--create {
  background: var(--liquid-primary-action);
  color: #fff;
  filter: brightness(1.06);
}
.nav__item--create .nav__glyph { width: 26px; height: 26px; }

.nav__badge {
  position: absolute;
  top: 5px;
  right: 5px;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--liquid-destructive);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--liquid-flat-chrome-fill);
}
.nav__dot {
  position: absolute;
  top: 11px;
  right: 11px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--liquid-destructive);
  box-shadow: 0 0 0 2px var(--liquid-flat-chrome-fill);
}

.nav__signout { margin: 0; display: flex; }

/* ── THE FLYOUTS ─────────────────────────────────────────────────────────────
 *
 * Opaque — the app's data-overlay family: a solid sheet, rows on it, the lit
 * row in the quiet fill. Anchored to the button that raised it, top-aligned,
 * and bottom-aligned for the foot buttons so Settings opens upward instead of
 * off the bottom of the window. */
.nav__menu {
  position: absolute;
  left: calc(100% + 12px);
  top: 0;
  z-index: 3;
  min-width: 248px;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  padding: 6px;
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-group);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.06);
}
.nav__fly--foot > .nav__menu { top: auto; bottom: 0; }

/* A BUTTON THAT IS A LINK — Channels, People, Profile, Settings (2026-09-24).
 * Pressing it opens its own screen; its flyout is a hover and keyboard-focus
 * extra above 700px and is never drawn on the phone (see partial-nav.html).
 * Only Create is still a <details>, which shows and hides its sheet itself. */
div.nav__fly > .nav__menu { display: none; }
@media (min-width: 701px) {
  div.nav__fly:focus-within > .nav__menu { display: flex; }
  div.nav__fly:focus-within > .nav__item { background: var(--liquid-fill-quiet); color: var(--liquid-primary-label); }
}
@media (min-width: 701px) and (hover: hover) {
  div.nav__fly:hover > .nav__menu { display: flex; }
  div.nav__fly:hover > .nav__item { background: var(--liquid-fill-quiet); color: var(--liquid-primary-label); }
  /* A bridge across the 12px gap, so the pointer travelling from the button to
   * its flyout does not leave the hover and close it on the way. */
  div.nav__fly > .nav__menu::before {
    content: "";
    position: absolute;
    right: 100%;
    top: 0;
    width: 14px;
    height: 100%;
  }
}

.nav__menutitle {
  margin: 0;
  padding: 8px 10px 6px;
  color: var(--liquid-secondary-label);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}

.nav__menurow {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 0 10px;
  border-radius: var(--liquid-radius-row);
  color: var(--liquid-primary-label);
  text-decoration: none;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.3;
}
.nav__menurow:hover,
.nav__menurow:focus-visible,
.nav__nest:hover > .nav__menurow,
.nav__nest:focus-within > .nav__menurow { background: var(--liquid-fill-quiet); outline: none; }
.nav__menurow--active { background: var(--liquid-fill-quiet); font-weight: 600; }
.nav__menurow .nav__glyph { width: 20px; height: 20px; color: var(--liquid-secondary-label); }
.nav__menurow--active .nav__glyph { color: var(--liquid-primary-label); stroke-width: 2.1; }
.nav__menurow .nav__chev {
  position: static;
  margin: 0 0 0 auto;
  width: 11px;
  height: 11px;
  color: var(--liquid-secondary-label);
}
.nav__menulabel {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE SECOND FLYOUT — a segment's channels. Opens on hover and on focus
 * within, so a pointer never clicks for it and Tab walks into it. It is the
 * only thing here that can be long, so it alone scrolls, with a thin bar. */
.nav__nest { position: relative; }
.nav__submenu {
  display: none;
  position: absolute;
  left: calc(100% + 6px);
  top: -6px;
  width: 280px;
  max-height: min(70vh, 520px);
  overflow-y: auto;
  scrollbar-width: thin;
  flex-direction: column;
  padding: 6px;
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-group);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.06);
}
/* A bridge across the gap, so the pointer crossing it does not close the
 * flyout it is travelling to. On the row, because the submenu scrolls and
 * would clip a pseudo-element hung outside itself. */
.nav__nest > .nav__menurow { position: relative; }
.nav__nest > .nav__menurow::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 0;
  width: 14px;
  height: 100%;
}
.nav__nest:hover > .nav__submenu,
.nav__nest:focus-within > .nav__submenu { display: flex; }

.nav__menurow--channel { gap: 10px; min-height: 42px; }
.nav__pic {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  object-fit: cover;
  overflow: hidden;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
}
.nav__count {
  flex: 0 0 auto;
  min-width: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--liquid-destructive);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}

/* ── THE WIDE RAIL (2026-09-24) ─────────────────────────────────────────────
 *
 * Above 700px each button is a full-width row: icon, then the label in place
 * of the old tooltip, then the badge / dot / chevron at the trailing edge.
 * Scoped to the desktop so the phone bar below keeps its own layout. The
 * flyouts are the same sheets as before and still open beside the rail. */
@media (min-width: 701px) {
  .nav { align-items: stretch; padding: 16px 12px; }
  .nav__rows,
  .nav__foot { align-items: stretch; }
  .nav__item {
    justify-content: flex-start;
    gap: 14px;
    width: 100%;
    height: 48px;
    padding: 0 12px;
    border-radius: var(--liquid-radius-row);
    font-weight: 500;
    text-align: left;
  }
  .nav__item--active { font-weight: 600; }
  .nav__label,
  .nav__item:hover .nav__label,
  .nav__item:focus-visible .nav__label,
  .nav__fly[open] > .nav__item .nav__label {
    position: static;
    flex: 1 1 auto;
    min-width: 0;
    transform: none;
    padding: 0;
    background: none;
    color: inherit;
    font-size: 15px;
    font-weight: inherit;
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 1;
    transition: none;
  }
  .nav__chev,
  .nav__badge,
  .nav__dot {
    position: static;
    flex: 0 0 auto;
    margin: 0;
    box-shadow: none;
  }
  .nav__item--create { margin-bottom: 16px; font-weight: 600; }
  .nav__signout,
  .nav__signout .nav__item { width: 100%; }
}

/* ── THE CENTRE COLUMN ────────────────────────────────────────────────────────
 *
 * `margin: 0` is load-bearing: .page centres itself with auto margins when it
 * is the whole document, and inside a flex row an auto margin swallows the free
 * space before justify-content runs, and the column would stop centring in
 * the room the left rail leaves. (The right rail beside it is retired —
 * 2026-09-24, see the RETIRED .rail note.) */
.shell .page { flex: 1 1 auto; min-width: 0; max-width: 640px; margin: 0; }


/* ── THE PHONE: THE APP'S BAR ───────────────────────────────────────────────
 *
 * Below 700px the rail becomes the app's floating tab bar — a capsule of the
 * rows WITHOUT nav__item--desk, with the create circle beside it — and a flyout
 * opens as a sheet above the bar. Not redesigned on 2026-09-21; this is the old
 * bar re-expressed over the new markup.
 *
 * SIZED TO THE APP'S BAR SINCE 2026-09-26. LiquidNavBar is 64 tall with a 64
 * create circle beside it; the web drew 58 and 52, and on a phone held next to
 * the app it read as a smaller copy. A tab is 54 inside 4px of track padding
 * and the hairline, which lands on 64. */
@media (max-width: 700px) {
  .shell { display: block; padding: 0; }
  .nav__item--desk,
  .nav__signout--desk,
  .nav__foot { display: none; }

  .nav {
    inset: auto 0 calc(16px + env(safe-area-inset-bottom, 0px)) 0;
    width: auto;
    flex-direction: row;
    justify-content: center;
    gap: 10px;
    padding: 0 12px;
    background: none;
    border: 0;
    pointer-events: none;
  }
  .nav > * { pointer-events: auto; }
  .nav__rows {
    flex-direction: row;
    gap: 2px;
    padding: 4px 6px;
    border-radius: var(--liquid-radius-capsule);
    /* A TRACK, NOT A PLATE — LiquidNavBar's flat pill (Palette.track). It was
     * the white chrome fill, which on the linen page left the bar barely
     * distinguishable from the body (reported 2026-09-28). The selected tab
     * is the white thumb below: grey track, white thumb, as the segmented
     * control draws it. */
    background: var(--liquid-track);
    border: 1px solid var(--liquid-content-border);
    box-shadow: var(--liquid-chrome-shadow);
  }
  .nav__fly { position: static; }
  .nav__item {
    flex-direction: column;
    gap: 3px;
    width: auto;
    min-width: 72px;
    height: 54px;
    padding: 0 8px;
    border-radius: var(--liquid-radius-capsule);
  }
  /* The app's _LiquidIndicator: opaque white in light, a white wash in dark,
   * each with its own shadow. */
  .nav__item--active {
    background: #fff;
    color: var(--liquid-accent);
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.10);
  }
  :root[data-theme="dark"] .nav__item--active {
    background: rgba(255, 255, 255, 0.16);
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.26);
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .nav__item--active {
      background: rgba(255, 255, 255, 0.16);
      box-shadow: 0 3px 9px rgba(0, 0, 0, 0.26);
    }
  }
  .nav__chev { display: none; }
  .nav__label {
    position: static;
    transform: none;
    padding: 0;
    background: none;
    color: inherit;
    font-size: 11px;
    opacity: 1;
    transition: none;
  }
  .nav__fly[open] > .nav__item .nav__label { opacity: 1; }
  .nav__badge { top: 0; right: auto; left: 50%; margin-left: 4px; }
  .nav__dot { top: 6px; right: auto; left: 50%; margin-left: 8px; }

  .nav__create { order: 2; }
  .nav__item--create {
    width: 64px;
    min-width: 0;
    height: 64px;
    margin: 0;
    padding: 0;
    box-shadow: var(--liquid-chrome-shadow);
  }
  .nav__item--create .nav__label { display: none; }
  .nav__item--create .nav__glyph { width: 28px; height: 28px; }

  .nav__menu {
    position: fixed;
    left: 12px;
    right: 12px;
    top: auto;
    bottom: calc(92px + env(safe-area-inset-bottom, 0px));
    max-width: none;
  }
  .nav__submenu,
  .nav__nest:hover > .nav__submenu,
  .nav__nest:focus-within > .nav__submenu { display: none; }
  .nav__menurow .nav__chev { display: none; }

  .shell .page { padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
  /* The app's gutter (LiquidSpace.gutter, 20) and the app bar's 8 above. */
  .page { padding: 8px 20px 16px; }

  /* THE BAR IS ON THE FOUR TABS AND NOWHERE ELSE (2026-10-03, user asked).
   * In the app the bar belongs to the shell, and every other screen — a
   * channel, a ping, Profile, Settings, a form — is pushed OVER it, so it
   * is not there and the screen's own action bar takes the foot alone. The
   * web drew it on every page, under or beside each action bar. body.app--tab
   * marks a tab root (navIsTabRoot, lib-nav-groups.go); off one, the back
   * chevron in the page head is the way out, as it is in the app. */
  .app:not(.app--tab) .nav { display: none; }
  .app:not(.app--tab) .shell .page { padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)); }
}
/* A finger leaves :hover behind on whatever it tapped, so a fill drawn for a
 * pointer stays on a tab, a chip or an action after the press. */
@media (hover: none) {
  .nav__item:not(.nav__item--active):not(.nav__item--create):hover {
    background: none;
    color: var(--liquid-secondary-label);
  }
  .chipbtn:not(.chipbtn--me):hover { background: var(--liquid-flat-chrome-fill); }
}
/* ── RETIRED 2026-09-24 — .app--phase1 (the release flag is gone; the phone bar always draws) ── */
/* ── RETIRED 2026-10-03 — `.page { padding: 12px 10px; }` below 480px: the
 * gutter is the app's 20 at every phone width, as it is in the app. ── */
@media (max-width: 480px) {
  /* Four 62s, the track, the gap and the 64 circle still fit a 360 phone. */
  .nav__item { min-width: 62px; padding: 0 4px; }
}

/* ── RETIRED — .rail, .rail__* (2026-09-24) ───────────────────────────────── * The right rail: an identity chip (avatar, name, @handle, Sign out) over a * five-channel Discover panel, desktop only. Removed at the user's request — * the left rail already carries Discover, Profile and Sign out, and the * column was a second copy of each (partial-rail.html said as much: "nothing * may be added here that is not reachable from the left rail"). */
/* ── The ad slot ────────────────────────────────────────────────────────────
 *
 * A manual Google AdSense unit — see templates/partial-ad-slot.html.
 *
 * THE MARGIN IS THE ACCIDENTAL-CLICK GUARD, not spacing taste. These slots sit
 * at the foot of a list whose last row is INTERACTIVE — a reaction button, a
 * link. A tappable ad flush against a tappable control is how a thumb aimed at
 * one lands on the other, and an accidental click is not revenue: it is invalid
 * traffic, which a network claws back and, repeated, suspends an account over.
 * The app widget enforces the same clearance in Dart — see AnchoredAdSlot.
 *
 * min-height reserves the slot BEFORE it fills, so the page does not shift
 * under a thumb at the moment the ad appears, which is the other half of the
 * same problem.
 *
 * UNFILLED MEANS GONE. AdSense marks an <ins> it could not fill with
 * data-ad-status="unfilled"; the whole slot, label and all, is hidden then, so
 * no page ever shows a labelled empty box. */
.ad-slot {
  display: block;
  margin: 24px 0 8px;
  padding-top: 12px;
  border-top: var(--liquid-hairline) solid var(--liquid-separator);
}

.ad-slot:has(ins.adsbygoogle[data-ad-status="unfilled"]) {
  display: none;
}

.ad-slot__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.ad-slot__label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--liquid-secondary-label);
}

/* "Privacy & cookie settings" — re-opens Google's consent dialogue. Quiet on
 * purpose: it must be findable wherever an ad is, and must not compete with it. */
.ad-slot__choices {
  appearance: none;
  background: none;
  border: 0;
  padding: 4px 0;
  font: inherit;
  font-size: 11px;
  color: var(--liquid-secondary-label);
  text-decoration: underline;
  cursor: pointer;
}

.ad-slot__unit {
  /* display:block is the rule AdSense's snippet carries in a style=""
   * attribute, which the CSP refuses — so it lives here. */
  display: block;
  min-height: 90px;
  /* No background and no border. An empty slot should read as empty space
   * rather than as a component that failed to load. */
}

/* ── RETIRED 2026-09-24 — .pills, .pill, .pill--active ─────────────────────
 * The destination row, `Discover · Channels · Pings`, mirroring the app's
 * HomePills: a horizontally scrolling capsule row, the lit one filled
 * --liquid-primary-action with a #A3E4C3 rim, hidden above 700px by its own
 * media query. partial-home-pills.html went with the Channels redesign — the
 * app's note is "the pill row is precisely what this redesign removed" — and
 * the Following | Publishing | Discover segments (.seg) replaced it. The rules
 * outlived the markup because `.pill` is a substring of `.pill-react`. */

/* ══════════════════════════════════════════════════════════════════════════
 * THE CATALOGUE: the search bar and the Discover card.
 *
 * Everything from here down was added when the front page stopped being three
 * rows and became the public catalogue. Roughly half of it is not new design at
 * all — it is CSS for classes the templates had been emitting all along with
 * NOTHING behind them: .input, .empty, .btn-primary, .channel post, .when, .label and
 * .actions were all in the markup and in no stylesheet, which is why the site
 * read as an unstyled document rather than as a broken one. An undefined class
 * fails silently and looks exactly like a stylesheet that did not load.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── the button names the templates already used ────────────────────────────
 *
 * .btn is the filled capsule, so .btn-primary is the same thing under the name
 * the public layout gave it. Kept as an alias rather than renamed across the
 * templates because partial-ping-blocks.html and the public pages share markup
 * with these, and one class that means the same in both stylesheets is the
 * point. .btn--quiet and .btn-secondary are likewise one treatment. */
.btn-primary { background: var(--liquid-primary-action); color: #fff; }

.btn-secondary,
.btn--quiet {
  background: var(--liquid-flat-chrome-fill);
  /* LiquidPillButton: flat chrome fill, hairline rim, label-coloured text. */
  color: var(--liquid-primary-label);
  border: 1px solid var(--liquid-content-border);
}

/* Links styled as buttons must not also be underlined. */
a.btn, a.btn-primary, a.btn-secondary { text-decoration: none; }

/* ── form controls ──────────────────────────────────────────────────────────
 *
 * font: inherit, or a search field renders in the browser default and is
 * visibly a different typeface from every label beside it. The 16px minimum is
 * not taste either: iOS Safari zooms the page when a focused input is smaller
 * than that, which on a search box means the catalogue jumps sideways the
 * moment somebody taps it.
 *
 * WHICH IS WHY THIS IS A max() AND NOT THE TOKEN ALONE. A field should grow
 * with the desktop tier like everything else, and --type-body does that — but
 * it is a rem, so a reader who has set a SMALLER browser default would drop
 * below 16 real pixels and get the zoom. max() takes whichever is larger and
 * both requirements hold at every width. */
.input {
  flex: 1;
  min-width: 0;
  /* LiquidTextField: 56 tall, 16 in, 16/500, a white content surface with the
   * content hairline, LiquidRadius.inner. */
  min-height: 56px;
  padding: 0 16px;
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  color: var(--liquid-primary-label);
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: 14px;
  -webkit-appearance: none;
  appearance: none;
}

.input::placeholder { color: var(--liquid-tertiary-label); }
.input:focus-visible { outline: none; border-color: var(--liquid-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--liquid-accent) 18%, transparent); }
textarea.input { padding: 16px; line-height: 1.4; }
/* The disclosure chevron, drawn with two gradients because .input drops the
 * native arrow and a data: image would need an img-src the CSP does not name. */
select.input {
  padding-right: 40px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--liquid-secondary-label) 50%),
    linear-gradient(135deg, var(--liquid-secondary-label) 50%, transparent 50%);
  background-position: calc(100% - 22px) 50%, calc(100% - 17px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* THE INLINE SEARCH WELL — InlineListSearchField: a 40pt capsule in
 * liquidSearchFieldFill (Palette.well), no outline, glyph leading, 16/500. */
.group--search { margin: 0 0 var(--space-section); }
.searchbar {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  margin: 0;
  padding: 0 6px 0 12px;
  border-radius: 20px;
  background: var(--liquid-fill-quiet);
}
.searchbar:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--liquid-accent) 40%, transparent); }
.searchbar__glyph { display: inline-flex; color: var(--liquid-tertiary-label); }
.searchbar__glyph .nav__glyph { width: 18px; height: 18px; stroke-width: 2; }
.searchbar__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  border: 0;
  background: transparent;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}
.searchbar__input::placeholder { color: var(--liquid-tertiary-label); }
.searchbar__input::-webkit-search-cancel-button { display: none; }
.searchbar__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  color: var(--liquid-secondary-label);
}
.searchbar__clear:hover { background: var(--liquid-fill-quiet); }
.searchbar__clear .nav__glyph { width: 16px; height: 16px; stroke-width: 2.2; }
/* RESULTS LOOK LIKE RESULTS (2026-10-04, user): a query in the address means
 * the list is search results — an accent hairline (inset, so the 40px capsule
 * does not grow) and the glyph and ✕ in the accent. The focus ring keeps the
 * hairline under it. */
.searchbar--active { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--liquid-accent) 55%, transparent); }
.searchbar--active:focus-within {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--liquid-accent) 55%, transparent),
              0 0 0 2px color-mix(in srgb, var(--liquid-accent) 40%, transparent);
}
.searchbar--active .searchbar__glyph,
.searchbar--active .searchbar__clear { color: var(--liquid-accent); }

/* THE DISCOVER TOPIC CHIPS (105, 2026-09-26) — ChannelCategoryChip: a 36pt
 * capsule on the content surface with its hairline, the channels accent and a
 * heavier weight when selected. Sits tight under the search well it narrows,
 * one sideways-scrolling row on a phone, wrapped where there is room. */
.catchips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: calc(-1 * var(--space-section) + 12px) 0 var(--space-section);
}
.catchip {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 14px;
  border-radius: 18px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-primary-label);
  font-size: 15px;
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
}
.catchip:hover { background: var(--liquid-fill-quiet); }
.catchip--on,
.catchip--on:hover,
.catchip:has(input:checked) {
  background: color-mix(in srgb, var(--liquid-channel-accent) 10%, var(--liquid-content-surface));
  border: 1.6px solid color-mix(in srgb, var(--liquid-channel-accent) 85%, transparent);
  color: var(--liquid-channel-accent);
  font-weight: 700;
}
.catchip:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
/* The editor row: radios, so a label is the chip. Always wrapped. */
label.catchip { cursor: pointer; }
.catchip:has(input:focus-visible) { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
.catchips--wrap { margin: 0; }
@media (max-width: 700px) {
  .catchips {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .catchips::-webkit-scrollbar { display: none; }
  .catchips--wrap { flex-wrap: wrap; overflow: visible; }
}

/* ── the empty state ────────────────────────────────────────────────────────
 *
 * grid-column spans the card grid rather than sitting in the first cell as a
 * one-column-wide paragraph, which is what it would do inside .cards. */
.empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 28px 14px;
  text-align: center;
  color: var(--liquid-secondary-label);
}

/* A section title outside a group — the catalogue heading. .group__header is
 * the small uppercase label ABOVE a panel; this is a heading over a grid of
 * cards and reads as one. */
.section-header {
  font-size: 20px;
  font-weight: 700;
  margin: 0 0 12px 2px;
}

/* ── THE DISCOVER CARD ──────────────────────────────────────────────────────
 *
 * The app's ChannelTile, in CSS. See templates/partial-channel-cards.html for why a
 * channel is not drawn as a .row like everything else on this site.
 *
 * # ONE COLUMN, AND THE GRID IS NOT FINDING ITS OWN COUNT ANY MORE
 *
 * It was `repeat(auto-fill, minmax(260px, 1fr))`, which inside a 640px content
 * column resolves to TWO. That looked like a considered desktop layout and was
 * really an accident of arithmetic: 260 was the width below which a name and a
 * handle stop fitting, so the grid packed in as many minimum-width cards as the
 * column allowed and every reader got the narrowest card the design supports.
 *
 * A channel card is not a grid tile. It is one publication being offered, with a
 * description a publisher wrote to be read and numbers a reader is meant to
 * weigh before spending credits — and the app presents them as a single column
 * of full-width tiles for that reason. Two half-width cards side by side halve
 * the description, halve the room the identity block has, and turn a list you
 * scan top to bottom into a page you read in a Z. The site now shows what the
 * app shows: one column, wider cards, the same order.
 *
 * THE ROW GAP IS LOAD-BEARING. The chips straddle the bottom edge, hanging most
 * of their height below the card, so a gap smaller than that overhang would let
 * one card's chips touch the card beneath it.
 *
 * minmax(0, 1fr), NOT 1fr (2026-10-04, parity audit): a bare 1fr track is at
 * least as wide as its widest unbreakable line, and .card__name is nowrap —
 * so a long channel name widened the column before its ellipsis could apply,
 * and Discover scrolled sideways on a 390px phone (516px for "Associação dos
 * Moradores do Bairro Alto e Bica"). The 0 floor lets the ellipsis work. */
.cards {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 26px;
  margin: 0 0 20px;
}

/* THE OUTLINE IS THE CARD, and its absence was the bug.
 *
 * This drew a fill and no border, and the fill is four points of lightness away
 * from the page behind it — so a "card" was a slightly paler rectangle with no
 * edge, and the FIRST time anybody could see where one ended was on hover,
 * where an outline appeared. That reads exactly backwards: a card that only
 * looks like a card when you point at it looks like a bug the rest of the time.
 *
 * The app never had the problem because LiquidContentSurface always draws both,
 * and ChannelTile passes channelCardBorder for the hairline. See --liquid-content-
 * border in tokens.css.
 *
 * 18px, from ChannelTile's own `radius: 18`, rather than the 14 a grouped panel
 * takes. A card is one object and a panel is a container with rows carved into
 * it; the corner is most of what says which you are looking at. */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 16px 26px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card);
  color: inherit;
  text-decoration: none;
}

/* ── RETIRED — .card:hover, AND THE SAME RULE NOW HOLDS FOR EVERY CARD ──────
 *
 *   .card:hover { background: var(--liquid-fill-quiet);
 *                 border-color: var(--liquid-separator); }
 *
 * A CARD DOES NOT CHANGE COLOUR UNDER THE POINTER. The rule went through two
 * shapes before it went away — an outline first, then a fill — and the second
 * was defensible on its own terms: with a real border there, thickening the
 * edge read as the edge being wrong rather than as a response.
 *
 * What neither version could answer is that THE APP HAS NO HOVER AT ALL. A card
 * on a phone is a surface you press, and pressing it lifts it into a context
 * menu; there is no state between "not touching it" and "acting on it", so a
 * card that shifts a shade as a mouse crosses it is inventing a third state
 * this product does not have. On a timeline that is twenty rectangles quietly
 * lighting and unlighting while the pointer travels to the scrollbar.
 *
 * IT ALSO STOPPED BEING TRUE. Every card carries a ⋯ menu now (see .cardmenu),
 * so a card is no longer one undivided target whose whole surface does one
 * thing — and a fill across all of it says otherwise.
 *
 * WHAT REPLACES IT IS THE CURSOR AND THE FOCUS RING, the two signals that
 * survive: `cursor: pointer` comes free with the anchor and says the thing is
 * clickable without repainting it, and :focus-visible is what a keyboard reader
 * navigates by and is NOT a hover. Dropping the ring would be an accessibility
 * regression; dropping the hover is not.
 *
 * ANYTHING GENUINELY ROW-SHAPED KEEPS ITS HOVER — .row--link, .rail__rowlink,
 * .cardmenu__item. A row inside a grouped panel has no border of its own, so
 * the fill is the only thing saying which of eleven rows the pointer is on.
 * That is a different problem from a bordered card's and it keeps its answer.
 */
.card:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }

/* A CARD IS ROOMIER ON A DESKTOP, and this is the other half of the two-tier
 * type scale at the top of the sheet.
 *
 * Growing the words inside a box whose padding is fixed does not make the card
 * bigger — it makes it TIGHTER, because the same 16px gutter now sits beside
 * 20px text instead of 17px. The two have to move together or the change reads
 * as cramped rather than as generous, which is the failure mode people
 * describe as "the fonts got bigger and it looks worse".
 *
 * The bottom stays deeper than the top on both tiers: the chips hang on that
 * edge (see .chiprow), and they need the room the other three sides do not. */
@media (min-width: 701px) {
  .card { gap: 12px; padding: 20px 22px 30px; }
}

.card__head { display: flex; align-items: center; gap: 12px; min-width: 0; }

/* The avatar is a ROUNDED SQUARE, not a circle, and that is the app's rule
 * rather than a preference: a circle reads as a person, and a channel is a
 * publication. 0.28 of the side is the ratio ChannelAvatar uses, so an avatar of
 * any size wears the same corner as the tiles beside it. */
.card__avatar {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 15px; /* 52 * 0.28, rounded */
  object-fit: cover;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  font-weight: 700;
  /* White on a saturated fill in both themes. The palette below is chosen so
   * this holds for every entry in it. */
  color: #fff;
  overflow: hidden;
}

/* THE EIGHT TINTS, COPIED FROM channelTintFor IN
 * lib/screens/channels/widgets/channel_tile.dart — values copied, not reinterpreted,
 * for the same reason the token layer copies its hexes: a channel that is one
 * orange in the app and a neighbouring orange on the web reads as a rendering
 * bug, where an obviously different colour would only read as different.
 *
 * Index 0 is the domain orange BY CONTRACT: it is what a channel with no tint of
 * its own falls back to, so the fallback and the first tint must agree. When
 * the domain went orange on 2026-09-04 index 0 followed it, and the violet it
 * displaced took slot 7 — which is why the last entry is the colour the whole
 * domain used to be. The SET is unchanged in both themes; only the ends moved.
 *
 * # SLOT 0 WAS #EA580C AND IS Palette.channelLight NOW — 2026-09-20
 *
 * The app moved its channel orange into `Palette` on 2026-09-17 and split it
 * three ways: channelLight #B85400, channelMid #BF5C08, channelDark #FFA24F.
 * channelTintsLight[0] is the LIGHT one, and this table had been copied from
 * before that split, so every untinted channel was a visibly different orange
 * in the two clients — which is precisely the "reads as a rendering bug"
 * failure the paragraph above is about.
 *
 * IT IS ALSO WHAT THE DATABASE HOLDS. Migration 093 stores an owner's chosen
 * colour as one of these eight hexes in its LIGHT spelling, so #B85400 is the
 * string a row carries; see channelColourChoices in lib-channel-cards.go, which
 * matches the old spelling too because a row written by an older build is still
 * somebody's choice.
 *
 * Go picks the INDEX and the stylesheet picks the colour, because Go does not
 * know which theme the reader is in. Go resolves through channelTint — the
 * owner's choice first, the hash second — and channelTintIndex is only the
 * fallback half of that.
 *
 * # EACH ENTRY PUBLISHES ITS COLOUR AS --tint AS WELL AS PAINTING IT
 *
 * Because a second thing needs the same hex and cannot be the same element: the
 * strip's selection halo, which the app draws as a ring AND a glow in the
 * CHANNEL's own colour (ChannelStripTile). It has to sit outside the square, so
 * it is a box-shadow on the square rather than a fill, and a box-shadow cannot
 * read the element's own background.
 *
 * Custom properties INHERIT, which is what makes this work for a channel that has
 * a picture: the tile's disc carries the class, so --tint is in scope for the
 * <img> inside it even though the image is what is painted. Without that, every
 * channel with a photograph would halo in the chrome accent and every channel
 * without one in its own colour — the same mark meaning two things.
 *
 * ADDING A NINTH TINT MEANS ADDING BOTH DECLARATIONS. A background without a
 * --tint beside it is a channel whose square is right and whose selection ring
 * silently falls back. */
.card__avatar--t0 { --tint: #B85400; background: var(--tint); } /* orange — the domain colour */
.card__avatar--t1 { --tint: #4F46E5; background: var(--tint); } /* indigo */
.card__avatar--t2 { --tint: #C026D3; background: var(--tint); } /* fuchsia */
.card__avatar--t3 { --tint: #E11D48; background: var(--tint); } /* rose */
.card__avatar--t4 { --tint: #D97706; background: var(--tint); } /* amber */
.card__avatar--t5 { --tint: #0284C7; background: var(--tint); } /* sky */

/* The dark half of the same palette, at the 0.85 alpha the app's placeholder
 * uses (D9). Both the media query and the explicit attribute, so the toggle
 * wins in either direction — the pattern every other themed value here
 * follows. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .card__avatar--t0 { --tint: #FFA24FD9; background: var(--tint); }
  :root:not([data-theme="light"]) .card__avatar--t1 { --tint: #7F86F0D9; background: var(--tint); }
  :root:not([data-theme="light"]) .card__avatar--t2 { --tint: #DC6BE8D9; background: var(--tint); }
  :root:not([data-theme="light"]) .card__avatar--t3 { --tint: #F2617FD9; background: var(--tint); }
  :root:not([data-theme="light"]) .card__avatar--t4 { --tint: #EFA83AD9; background: var(--tint); }
  :root:not([data-theme="light"]) .card__avatar--t5 { --tint: #4BA6E0D9; background: var(--tint); }
}

:root[data-theme="dark"] .card__avatar--t0 { --tint: #FFA24FD9; background: var(--tint); }
:root[data-theme="dark"] .card__avatar--t1 { --tint: #7F86F0D9; background: var(--tint); }
:root[data-theme="dark"] .card__avatar--t2 { --tint: #DC6BE8D9; background: var(--tint); }
:root[data-theme="dark"] .card__avatar--t3 { --tint: #F2617FD9; background: var(--tint); }
:root[data-theme="dark"] .card__avatar--t4 { --tint: #EFA83AD9; background: var(--tint); }
:root[data-theme="dark"] .card__avatar--t5 { --tint: #4BA6E0D9; background: var(--tint); }

/* ── THE CHANNEL STRIP ────────────────────────────────────────────────────────
 *
 * The row of publications above the timeline, at "/" and at /channels. It reuses
 * the catalogue card's TINT classes (.card__avatar--tN, above) and nothing
 * else: the two shapes have different jobs — a card is judged, a tile is
 * reached for — and sharing the layout would make one of them wrong.
 *
 * # IT SCROLLS SIDEWAYS AND THE PAGE DOES NOT
 *
 * overflow-x on the strip itself, so a reader with thirty channels scrolls the
 * row rather than the document. Every wide thing on this site is contained
 * this way; a body that scrolls horizontally is the failure it prevents.
 *
 * # AND IT HAS TO LOOK LIKE IT SCROLLS, WHICH IS WHERE THIS WENT WRONG
 *
 * `scrollbar-width: none` was here from the start, with a note saying the tiles
 * are plainly a row. On a TOUCH device that is true — a finger drags the row
 * and nothing else is needed. On a DESKTOP it is false twice over: a mouse
 * wheel scrolls the PAGE, not a horizontal container under the pointer, and
 * with the bar hidden there is no second way in. The row simply stopped at the
 * column edge, and everything past it — including Manage, which is the last
 * tile by design — was unreachable and read as missing. That is exactly how it
 * was reported: "the strip does not scroll and Manage is gone".
 *
 * THE FIX IS A THIN BAR ON POINTER DEVICES AND NOTHING ON TOUCH. `@media
 * (hover: hover) and (pointer: fine)` is the honest test — it asks whether
 * there IS a pointer, rather than guessing from width, so a touch laptop gets
 * the touch answer and a phone with a mouse gets the pointer one.
 *
 * A BAR AND NOT ARROW BUTTONS. Arrows are two more controls to place, to
 * disable at each end, to keep out of the tab order and to translate, and they
 * are the affordance a scrollbar already is. The bar is styled to the site's
 * own tokens so it reads as part of the strip rather than as an OS artefact.
 *
 * scroll-padding matches the row's own inset so a tile snapped to the start
 * does not sit under the left edge of the column. */

/* ── THE HEADER OVER A FILTERED TIMELINE ─────────────────────────────────────
 *
 * Which channel the strip is narrowed to, and the way into it. Drawn only under
 * ?channel=, because with no tile selected there is no one channel to open.
 *
 * # WHY IT EXISTS AT ALL
 *
 * A strip tile NARROWS this list rather than opening the channel, and
 * /channels/manage — which used to be the other way in — is retired. Without
 * this row the channel's own page would be reachable only from an update card's
 * ⋯ menu, which fails for exactly the channel a publisher most wants to open:
 * the one with nothing on it yet.
 *
 * NOT A .group. It is a line ABOUT the list below it, not a card beside it, so
 * it takes the page background and the section rhythm rather than a surface of
 * its own — the same reasoning that took the .group off the timeline itself. */
.listhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-row);
  margin: 0 0 var(--space-header);
}

.listhead__name {
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The link keeps the sheet's link colour and drops the underline, like every
 * other trailing "see all" on this site: it is a control at the end of a row,
 * and an underline there reads as prose. */
.listhead__link {
  flex: 0 0 auto;
  font-size: var(--type-small);
  font-weight: 600;
  text-decoration: none;
}

/* ── THE STRIP'S GEOMETRY IS THE APP'S, NUMBER FOR NUMBER — 2026-09-20 ───────
 *
 * home_strips.dart states it as named constants and computes the row's height
 * from them; these are those constants. They were eyeballed here before, and
 * close is the one thing a second implementation of a shape must not be: a tile
 * four points wider and a square four points smaller reads as the same control
 * rendered badly, where an obviously different control would only read as
 * different.
 *
 *   _stripTile      64     the column one channel occupies
 *   _stripGap       10     between two of them
 *   _stripSquare    56     the picture
 *   _stripRadius    16     its corner
 *   _stripRingGap    4     daylight between the square and its halo —
 *                          64 = 56 + 2 * 4, so the halo reaches the tile's own
 *                          bounds and can never touch a neighbour
 *   _stripLabelGap   6
 *   caption         11px over TWO lines at 1.15
 *
 * THE CAPTION IS TWO LINES BECAUSE THE APP'S IS. One line ellipsised at 64px
 * turns most publications into their first word and a full stop — "Weekly…" —
 * which is exactly the width at which a row of tiles stops being recognisable.
 * The row reserves both lines whether or not a name needs them, so a strip does
 * not change height as its contents change.
 *
 * # THE WEB IS A STEP LARGER NOW — 2026-09-22
 *
 * At the owner's request, since a desktop column has room the phone lacks:
 * tile 76, square 64, corner 18, caption 12px, gap 12. The table above is
 * still the app's; the proportions (ring gap 4, two caption lines) are kept. */
.channelstrip {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  /* ROOM FOR THE HALO, THE GLOW AND THE BADGE, which all sit outside the
   * square. It was 4px on top and 2px at the sides, and a scroller clips
   * whatever overflows it, so a selected tile lost the top of its ring and the
   * first tile its left side (reported 2026-09-22). The negative margin gives
   * the room back so the row still lines up with the column. */
  padding: 12px 10px 8px;
  margin: -8px -10px 4px;
  /* The row is a rail of targets: stopping on one is more useful than landing
   * between two. */
  scroll-snap-type: x proximity;
  scroll-padding-left: 10px;
  -webkit-overflow-scrolling: touch;
  /* NO BAR ON TOUCH, where a finger is the affordance. The pointer query below
   * puts one back for everybody else — see the note above, and note that this
   * is the DEFAULT rather than the exception because a phone is where most of
   * this traffic arrives. */
  scrollbar-width: none;
}
.channelstrip::-webkit-scrollbar { display: none; }

/* A POINTER GETS A BAR, because a wheel does not scroll this and a hidden bar
 * left the far end of the row unreachable. Thin, in the site's own separator
 * colour, so it reads as part of the strip.
 *
 * BOTH SYNTAXES, deliberately: `scrollbar-width`/`scrollbar-color` are the
 * standard and are what Firefox implements, `::-webkit-scrollbar` is what
 * Chrome and Safari still need. Setting one and not the other leaves half the
 * browsers with the bug this rule exists to fix. */
/* EVERY SIDEWAYS ROW'S BAR, IN ONE PLACE — 2026-09-22.
 *
 * [data-hscroll] marks the rows (channel, sender and circle strips, photo
 * galleries); layout.html turns a mouse wheel over one into a sideways scroll,
 * so the bar is no longer the only way along and can be nearly invisible:
 *
 *   touch     no bar at all; a finger drags the row
 *   pointer   a 4px bar, transparent until the row is hovered, then the
 *             separator colour
 *
 * BOTH SYNTAXES: scrollbar-width/-color is the standard (Firefox, and Chrome
 * since 121, which then ignores the -webkit- rules); ::-webkit-scrollbar is
 * what Safari still needs. */
[data-hscroll] { scrollbar-width: none; }
[data-hscroll]::-webkit-scrollbar { display: none; }
@media (hover: hover) and (pointer: fine) {
  [data-hscroll] { scrollbar-width: thin; scrollbar-color: transparent transparent; }
  [data-hscroll]:hover { scrollbar-color: var(--liquid-separator) transparent; }
  [data-hscroll]::-webkit-scrollbar { display: block; height: 4px; }
  [data-hscroll]::-webkit-scrollbar-track { background: transparent; }
  [data-hscroll]::-webkit-scrollbar-thumb { background: transparent; border-radius: 2px; }
  [data-hscroll]:hover::-webkit-scrollbar-thumb { background: var(--liquid-separator); }
}

.channelstrip__tile {
  flex: 0 0 auto;
  width: 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  scroll-snap-align: start;
  color: inherit;
  text-decoration: none;
}

/* THE SELECTED TILE, which the strip did not have until a tile started
 * filtering rather than leaving.
 *
 * The app draws a ring around the square and puts the label in the primary
 * colour; this is the same two marks. A ring rather than a fill because the
 * square is already the channel's own colour and tinting it again would say
 * something about the CHANNEL rather than about the selection.
 *
 * TWO MARKS AND NOT ONE. A ring alone is invisible to somebody who cannot
 * separate the accent from the eight tints beside it, and a bolder label alone
 * is a weak signal at 11px. Together they survive either failure — which is the
 * same argument the nav rows make for pairing aria-current with a colour.
 *
 * # THE RING IS THE CHANNEL'S COLOUR NOW, AND IT WAS THE CHROME ACCENT — 2026-09-20
 *
 * ChannelStripTile paints the halo and its glow in `tint` — the same value the
 * square beside it is filled with — so a lit tile is MORE of the channel it
 * already is. The blue here said only "selected", which is what a segmented
 * control says, and made every channel's selection look identical.
 *
 * --tint comes off the disc's own tint class, so a channel with a photograph
 * halos in its colour too; see the tint table for why the property exists.
 * The fallback is the accent, which is what All takes — All is not a channel
 * and has no tint to be more of.
 *
 * FOUR POINTS OF DAYLIGHT FIRST (_stripRingGap), in the page's background, and
 * then the ring. Ring and fill are the same colour, so without a gap of
 * background between them the ring is a slightly darker edge on a square of the
 * same hue — which is the version of this the app tried first.
 *
 * # TWO DECLARATIONS, AND THE SECOND IS THE GLOW
 *
 * The app puts a blurred shadow in the same colour behind the ring, so a lit
 * tile reads as lit at a glance rather than on inspection. Getting `tint` to
 * 32% alpha needs color-mix(), which is not everywhere yet — and a box-shadow
 * list with one unparseable colour in it is an INVALID DECLARATION, so a single
 * rule would drop the ring as well as the glow on the browsers that lack it.
 *
 * So the ring is stated alone first and the glow-bearing copy overrides it. A
 * browser without color-mix keeps the first and loses only the ornament, which
 * is the failure this ordering is chosen for. */
.channelstrip__tile--selected .channelstrip__disc {
  box-shadow:
    0 0 0 4px var(--liquid-grouped-background),
    0 0 0 6.5px var(--tint, var(--liquid-accent));
}
.channelstrip__tile--selected .channelstrip__disc {
  box-shadow:
    0 3px 12px color-mix(in srgb, var(--tint, var(--liquid-accent)) 32%, transparent),
    0 0 0 4px var(--liquid-grouped-background),
    0 0 0 6.5px var(--tint, var(--liquid-accent));
}
.channelstrip__tile--selected .channelstrip__name {
  color: var(--liquid-primary-label);
  font-weight: 600;
}
.channelstrip__tile:focus-visible {
  outline: 2px solid var(--liquid-accent);
  outline-offset: 3px;
  border-radius: 12px;
}

/* THE DISC IS THE SQUARE ITSELF NOW, AND IT USED TO BE A WRAPPER OF NO SIZE.
 *
 * Two things needed that change. The badge hangs off it, so it has to be the
 * positioned box — anchoring to the tile would move the badge when a name wraps
 * onto its second line. And the PICTURE IS A LAYER OVER IT rather than the
 * square itself, which is what makes a channel whose image fails still look like
 * a channel; see .channelstrip__mark.
 *
 * It carries the tint class, so --tint is in scope for the halo whether or not
 * there is a photograph on top, and the tinted square is what shows while one
 * loads. _stripRadius on the box as well as on the image, or the tint would
 * square off at the corners behind a rounded picture. */
.channelstrip__disc {
  position: relative;
  display: block;
  width: 64px;
  height: 64px;
  border-radius: 18px;
}

/* WHAT THE SQUARE SHOWS WHEN THERE IS NO PICTURE — WHICH IS ALSO WHAT IT SHOWS
 * UNDER ONE, WHILE IT LOADS AND IF IT FAILS.
 *
 * The app's ChannelStripTile has this exact note, and it is there because the
 * picture used to be a decoration on the square: a URL that 404s — a channel
 * image deleted from the bucket, a device with no route to the CDN — painted
 * nothing and STOPPED, so every tile whose picture failed looked like a tile
 * with nothing in it. That is exactly how "the strip doesn't show the images"
 * reads from the outside.
 *
 * Here the same failure has a second form: a bare <img> that cannot load draws
 * the browser's own broken-image marker, which is worse than empty. So the mark
 * is always in the document, underneath, and the image is painted over it. The
 * three states are then one square in three conditions rather than three
 * different-looking tiles:
 *
 *   no picture   the tinted square with its initial
 *   loading      the same square, with the picture arriving over it
 *   failed       the same square, unchanged
 *
 * 20px is the app's own figure for this glyph, not the 22px the square used to
 * set — that one was sized for a square four points smaller. */
.channelstrip__mark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 18px;
  font-size: 22px;
  font-weight: 700;
  color: #fff;
}
.channelstrip__mark svg { width: 24px; height: 24px; }

.channelstrip__img {
  position: absolute;
  inset: 0;
  width: 64px;
  height: 64px;
  border-radius: 18px;
  object-fit: cover;
  /* No alt text is drawn on a failure, because the mark underneath is the
   * answer to one — the element only has to get out of the way. */
  color: transparent;
}

/* ALL IS THE CHANNEL ORANGE SINCE 2026-09-28 — Palette.channel, as the app's
 * home_channel_strip.dart. Teal meant nothing, and the lagoon beside it now
 * means people. channelTintIndex no longer derives slot 0, so no uncoloured
 * channel can be All's twin. The note below is the teal's reasoning, kept for
 * the history of why All cannot be grey.
 *
 * ALL WAS TEAL, AND THAT WAS A NARROW ANSWER TO A NARROW QUESTION.
 *
 * It is the leading tile and it is selected by default, so it cannot be grey:
 * All and Manage began as the same neutral at opposite ends of one row, which
 * made the strip look like it had two of whatever they were, and darkening All
 * to ink then left a saturated row of publications led by a charcoal square —
 * which reads as the one tile that is switched OFF, and it is the one that is
 * ON.
 *
 * Teal is what is LEFT. It must not be any of channelTintFor's eight (violet,
 * indigo, fuchsia, rose, amber, sky, purple, orange) or a single-channel strip
 * puts an identically coloured All beside it; not the emerald a filled button
 * takes; and not the cyan a place ping owns. These are the app's own two
 * values — teal-700 light, teal-500 dark — so the tile is the same colour in
 * both places somebody sees it.
 *
 * The dark value is a step lighter rather than the same hex at lower alpha: the
 * eight tints above go translucent in dark mode because they sit under a photo
 * or an initial, and this one sits under a stroked glyph that needs the
 * contrast.
 *
 * IT PUBLISHES --tint LIKE THE EIGHT DO, so All's selection halo is All's own
 * colour rather than the chrome accent. The ring means "this tile is the one",
 * and a blue ring around the only teal square in the row would look like a
 * different kind of mark. All is still not a channel and takes no tint from
 * channelTintFor — this is the ONE colour it has, named once. */
.channelstrip__disc--all {
  --tint: #B85400;
  background: var(--tint);
  color: #fff;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .channelstrip__disc--all { --tint: #FFA24F; background: var(--tint); color: #1F1B16; }
}
:root[data-theme="dark"] .channelstrip__disc--all { --tint: #FFA24F; background: var(--tint); color: #1F1B16; }

.channelstrip__badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  box-sizing: border-box;
  border-radius: 9px;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  /* Reads as a badge ON the tile rather than a shape beside it. */
  box-shadow: 0 0 0 2px var(--liquid-grouped-background);
}

/* TWO LINES, THEN ELLIPSISED, AND THE HEIGHT IS RESERVED EITHER WAY.
 *
 * It was one line, which at 64px turns most publications into their first word:
 * "Weekly Notes From The Studio" became "Weekly…". Two is what the app gives it
 * (_stripLabelMaxScale over two line boxes is what _stripHeight is computed
 * from), and two is enough for almost every real name.
 *
 * min-height RATHER THAN A CLAMP ALONE. Without it a strip of one-word names is
 * a line shorter than a strip with a two-word name in it, so the timeline below
 * jumps as the reader switches segment — the same failure the app avoids by
 * deriving a fixed row height rather than letting the tallest tile decide. */
.channelstrip__name {
  max-width: 76px;
  font-size: 12px;
  line-height: 1.15;
  min-height: calc(2 * 1.15 * 12px);
  text-align: center;
  color: var(--liquid-secondary-label);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* A long single word — a handle, a URL — has no space to break at and would
   * otherwise push the tile wider than its 64px column. */
  overflow-wrap: anywhere;
}

/* min-width on the flex child, or a long name refuses to shrink and pushes the
 * card wider than its grid cell instead of ellipsing. */
.card__id { min-width: 0; display: flex; flex-direction: column; gap: 2px; }

.card__name {
  font-size: var(--type-title);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card__handle {
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Following", on a catalogue card for a channel the reader already has.
 *
 * It only appears under a SEARCH — the browse list excludes followed channels in
 * SQL, so it cannot hold one — and it is a LABEL rather than a disabled control:
 * the card offers no Follow button to disable.
 *
 * QUIET ON PURPOSE. It sits in the identity block under the handle, in the
 * channel accent at the caption size, with no fill: the card's one loud element
 * is the publisher's name, and a filled badge there would make an existing
 * follow read as a recommendation. margin-top rather than a gap on the parent,
 * because .card__id stacks two lines that are one thing (a name and its handle)
 * and this is a third thing under them — the tight/row distinction spacing.dart
 * draws. */
.card__badge {
  margin-top: var(--space-tight);
  font-size: var(--type-caption);
  font-weight: 600;
  color: var(--liquid-channel-accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Two lines, clamped, exactly as the app's tile clamps it. A description is a
 * publisher's own text with no length limit worth relying on, and a card that
 * grew to fit one would break the row rhythm for every card beside it. */
.card__desc {
  margin: 0;
  font-size: var(--type-small);
  color: var(--liquid-secondary-label);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

/* THE EDGE CHIPS — facts left, response right, straddling the bottom border.
 *
 * The app moved these off a full-width footer band and onto the edge to give
 * back about thirty-five points per tile on a surface somebody scrolls. The
 * shape is what separates a channel from every other card at a glance: nothing
 * else in the product carries anything on its edge.
 *
 * translateY is what makes them straddle rather than sit inside, and it is why
 * .cards has a 26px row gap. pointer-events are off so a chip cannot swallow a
 * click meant for the card it is hanging from. */
.card__chips {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 0;
  /* 8 of the chip's 24 sits ON the card and 16 hangs below it, which is
   * CardEdgeChips.overlap exactly. Caught by its top third it reads as hung on
   * the card from outside — a note ABOUT the thing. Cut in half by the edge it
   * reads as a footer that slipped down. */
  transform: translateY(calc(50% + 4px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  pointer-events: none;
}

/* THE CAPSULE. It is the CARD'S OWN MATERIAL — same fill, same hairline — so
 * the part hanging over the page is drawn in exactly what the part over the
 * card is drawn in and the two read as one object. That is the app's rule
 * (CardEdgeChips takes the card's borderColor rather than inventing one) and it
 * is why this is not the separator grey it used to be: an outlined card with
 * two foreign-coloured capsules resting on it reads as somebody else's chips. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 24px;
  padding: 0 9px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-flat-chrome-fill);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-secondary-label);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A busy reaction chip and a wide count chip are anchored to opposite corners
 * of the same edge, so with nothing stopping them they meet in the middle and
 * overlap into one unreadable strip. CardEdgeChips.maxChipFraction, kept.
 *
 * SCOPED TO THE CATALOGUE CARD, whose .card__chips is a full-width flex box
 * with two known ends. The channel post card's chips sit inside content-sized wrappers
 * where a percentage max-width resolves against a box that is itself sized by
 * its content, and the reaction pills elsewhere are not on an edge at all. */
.card__chips > .chip {
  max-width: 45%;
  overflow: hidden;
}

/* ONE GLYPH AND ONE NUMBER — the unit a chip is built out of (CardEdgeStat).
 *
 * The chips read "1  4" before this: two bare integers in two separate
 * capsules, with nothing on the card to say which was followers and which was
 * channel posts. The glyph is what makes a count a fact rather than a digit, and it is
 * why the app has never drawn one of these without one. */
.stat { display: inline-flex; align-items: center; gap: 4px; }
.stat__glyph { flex: 0 0 auto; width: 14px; height: 14px; }
.stat__n { font-size: 12px; font-weight: 600; }

/* The response chip takes the channels accent. One domain accent per act — orange
 * for channels, blue for pings — and this is the only place on the card where the
 * domain says which room you are standing in.
 *
 * THROUGH THE TOKEN, not through a hex and two dark overrides. It had all three
 * — #EA580C here and #FB923C twice below — which is three places kChannelPrimary
 * could be changed in and one of them forgotten. --liquid-channel-accent is that
 * pair, declared where every other themed value is. */
.chip--reaction {
  color: var(--liquid-channel-accent);
  /* TIGHTER THAN .chip's 8px, which is the space between two whole STATS in the
   * chip beside this one. Here the gap separates a row of emoji from the number
   * they add up to, and 8px between those two reads as two chips that failed to
   * separate. */
  gap: 5px;
}

/* The emoji, in their own box so they can sit shoulder to shoulder while the
 * chip's own gap keeps the number clear of them — one flex container cannot
 * have two gaps. Same construction and same numbers as .pill-react__glyphs,
 * because it is the same object at a smaller size.
 *
 * NO COLOUR IS SET, AND THAT IS DELIBERATE. .chip--reaction paints its text in
 * the channel accent, which is right for the number and would be wrong here if it
 * applied — an emoji is a colour glyph and ignores `color` entirely on every
 * platform that has an emoji font. The one case where it does not is a text
 * fallback with no such font, and there an accent-tinted glyph is better than a
 * black one on an orange chip. Nothing to do either way. */
.chip__glyphs {
  display: inline-flex;
  gap: 1px;
  font-size: 13px;
  line-height: 1;
}

/* The link to the rest of the catalogue, and the "load more" a channel post list uses.
 * Centred on its own line rather than inline, so it does not read as the last
 * item of the list above it. */
.loadmore { display: flex; width: fit-content; margin: 4px auto 20px; }

/* ── classes the templates emitted with nothing behind them ─────────────────
 *
 * These are not new. Every one was already in the markup — the channel post list, the
 * ping blocks shared with the public pages, the reaction row and the settings
 * forms — and none had a rule in this stylesheet, only in the public layout's
 * inline copy. The values are that copy's, expressed in tokens. */
/* ── RETIRED — .channel post, .channel post:first-child, .channel_post__title
 *
 * The bordered row a timeline entry used to be. Nothing on an app page emits it
 * any more: the timeline and a channel's own page draw .pcard, which carries the
 * attribution, the menu and the edge chips that a row had nowhere to put.
 *
 * THE PUBLIC PAGES STILL USE .channel post AND ARE UNAFFECTED, which is why the class
 * survived the change and this note has to say so: page-public-channel.html and
 * page-public-channel-post.html render through layout-public.html, whose CSS is INLINE
 * (partial-public-css.html) and has its own copy. Deleting the rule there would
 * break them; deleting it here breaks nothing, and leaving it here would leave a
 * rule that looks live and governs nothing.
 *
 * A row is still the right shape on those pages, so this is not a change they
 * are waiting for: a public page shows one channel or one channel post to a stranger, where
 * a card's attribution header would repeat what the page's own heading already
 * said. */
.when { color: var(--liquid-secondary-label); font-size: var(--type-caption); margin: 0; }

.blocks p {
  font-size: var(--type-body);
  line-height: 1.6;
  margin: 0 0 16px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ── RETIRED — .blocks img ──────────────────────────────────────────────────
 *
 *   .blocks img { display:block; max-width:100%; height:auto; border:0;
 *                 border-radius:12px; margin:0 0 16px;
 *                 background: var(--liquid-fill-quiet); }
 *
 * ONE FULL-WIDTH PICTURE PER IMAGE BLOCK, stacked. It is the arrangement the
 * app retired on 2026-09-08 and it went here for the same two reasons: a channel post
 * with six photographs was six screens of scroll, and a channel post with ONE was a
 * different SHAPE of object from a channel post with two — so a list holding both read
 * as two designs interleaved.
 *
 * There is no <img> left inside .blocks that this could match: every picture is
 * a .gallery__img now, and the grouping into runs happens in pingBlocks.Groups
 * because html/template has no lookahead. */

/* ══ THE PHOTO STRIP ═══════════════════════════════════════════════════════
 *
 * A run of consecutive image blocks, drawn as ONE horizontally scrolling row at
 * a fixed height with each tile as wide as its own photograph asks. The app's
 * PingImageGallery, in CSS.
 *
 * # NOTHING COMPUTES A WIDTH, AND NOTHING CAN
 *
 * The app works out `height * clampedRatio` per tile. This CSP grants no
 * 'unsafe-hashes', so a style="" attribute is dead markup and a per-image width
 * has nowhere to land — see the long note in lib-ping-blocks.go.
 *
 * It is not needed. The <img> carries the stored dimensions as ATTRIBUTES,
 * which every browser reads as an intrinsic aspect ratio before a byte arrives;
 * a fixed height and `width: auto` turn that ratio into a box, and the row is
 * laid out correctly on the first paint rather than reflowing as pictures land.
 *
 * # THE CLAMP IS WHAT KEEPS IT A ROW
 *
 * min-width and max-width are the app's own 0.45 and 1.78 — 9:20, the
 * tallest common phone frame, and 16:9 — against the same strip height.
 * Unclamped, a panorama takes three screens of scroll on its own and a tall
 * strip of a crop is a sliver beside it. Only the EXTREMES move: 4:3, 3:2,
 * 16:9 and every portrait frame down to 9:20 are inside the range and are
 * drawn at their exact proportions, which covers essentially every photograph
 * a phone produces. object-fit: cover crops the long dimension of the few that
 * are outside it, which is the dimension with the least in it.
 *
 * # THE PORTRAIT FLOOR IS A PHONE'S OWN FRAME (2026-10-04)
 *
 * It was 0.62, a shade narrower than 2:3, which put every 9:16, 9:19.5 and
 * 9:20 shot OUTSIDE the range: cover cut its top and bottom off, on the cards
 * and on the reading pages alike. The app had the same floor and the same cut.
 *
 * # TALLER, NOT NARROWER (2026-10-04)
 *
 * On a reading page a 9:20 frame at strip height would be 99px wide. So the
 * run grows instead: .gallery--tall-1 … -3 (chosen in Go, runTallness) set
 * --gallery-grow, which every height and width below is multiplied by, so the
 * narrowest tile is as wide as the old 0.62 floor drew it — 242px for 9:16 and
 * 303px for 9:20 at the 220px base, the app's own pingImageRunHeight. A card's
 * strip (.gallery--card) is never given the class and keeps its height.
 *
 * # THE ROW TAKES THE HORIZONTAL DRAG AND LEAVES THE VERTICAL ONE ALONE
 *
 * overscroll-behavior-x stops a flick that reaches the end of the strip from
 * chaining into the page — or, on a trackpad, into the browser's back gesture,
 * which is the one failure here that loses the reader their place entirely. */
.gallery {
  display: flex;
  gap: 6px;
  margin: 0 0 16px;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
}

.gallery__tile {
  position: relative;
  flex: 0 0 auto;
  display: block;
  border-radius: 12px;
  overflow: hidden;
  background: var(--liquid-fill-quiet);
  scroll-snap-align: start;
  /* An <img> is an inline box, so the anchor around it would otherwise be four
   * pixels taller than the picture and the tiles would not line up. */
  line-height: 0;
}

/* ── THE LOADING RING (2026-09-24) ──────────────────────────────────────────
 *
 * Drawn BEHIND the picture, not over it: the <img> is raised above the ring, so
 * the ring shows through only while the image has painted nothing yet, and a
 * loaded photograph covers it with no script involved at all. The lightbox
 * script marks the tile .is-loaded on load (or error) to stop the animation;
 * where that script is absent — the public share pages — the ring simply stays
 * hidden under an opaque photograph. */
.gallery__tile::before,
.lightbox__spinner {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  border: 2.5px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: img-spin 0.8s linear infinite;
}
.gallery__tile::before { color: var(--liquid-tertiary-label); }
.gallery__tile.is-loaded::before { display: none; }
@keyframes img-spin { to { transform: rotate(360deg); } }

.gallery__img {
  position: relative;
  z-index: 1;
  display: block;
  height: calc(var(--gallery-h) * var(--gallery-grow, 1));
  width: auto;
  /* RETIRED (2026-10-04) — see "THE PORTRAIT FLOOR" above:
   * height: var(--gallery-h);
   * min-width: calc(var(--gallery-h) * 0.62);
   * max-width: calc(var(--gallery-h) * 1.78); */
  min-width: calc(var(--gallery-h) * var(--gallery-grow, 1) * 0.45);
  max-width: calc(var(--gallery-h) * var(--gallery-grow, 1) * 1.78);
  object-fit: cover;
  border: 0;
}

/* Each step's factor is 0.62 over its NARROWEST ratio, so no tile in it is
 * narrower than the old floor: 9:16, 9:18, 9:20. */
.gallery--tall-1 { --gallery-grow: 1.1023; }
.gallery--tall-2 { --gallery-grow: 1.24; }
.gallery--tall-3 { --gallery-grow: 1.3778; }

/* A BLOCK THAT PREDATES THE STORED DIMENSIONS. A guessed ratio reflows worse
 * than no ratio, so the markup omits the attributes entirely and this reserves
 * the 4:3 box aspectRatioOf falls back to in the app. */
.gallery__img--unsized { aspect-ratio: 4 / 3; }

/* ── ONE PICTURE NEVER SCROLLS ──────────────────────────────────────────────
 *
 * Not a second layout — the same object at a different length, which is the
 * half of this the app corrected twice. A 16:9 frame at strip height is wider
 * than the column it sits in, so the honest strip would put the right edge of a
 * lone landscape photograph off screen and ask the reader to drag a row of one.
 *
 * Both maxima with `width: auto; height: auto` is the browser's own
 * shrink-to-fit: the picture is drawn whole, as large as fits the column and no
 * taller than the strip. That is exactly `min(stripHeight, maxWidth / ratio)`,
 * expressed where the numbers already are — and it CROPS NOTHING, which the
 * strip's `cover` deliberately does not promise.
 *
 * Deliberately not applied to a run. Two pictures that do not fit are what the
 * horizontal scroll is for, and shrinking them would make a channel post's photographs
 * smaller the more of them there are. */
.gallery--solo { display: block; overflow: visible; }
.gallery--solo .gallery__tile { display: inline-block; max-width: 100%; }
.gallery--solo .gallery__img {
  height: auto;
  width: auto;
  min-width: 0;
  max-width: 100%;
  max-height: calc(var(--gallery-h) * var(--gallery-grow, 1));
  object-fit: fill;
}

/* A phone's column is about 340px wide, where the desktop's is 608. A strip
 * that keeps its full height there is two thirds of the screen for the
 * pictures attached to a channel post rather than for the channel post. */
@media (max-width: 480px) {
  .gallery { --gallery-h: 176px; }
}

.blocks audio { display: block; width: 100%; margin: 0 0 16px; }
.blocks .voice-note { color: var(--liquid-secondary-label); font-style: italic; margin: 0 0 8px; }

.blocks .linkcard {
  display: block;
  text-decoration: none;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: 12px;
  padding: 12px 14px;
  margin: 0 0 16px;
}
.blocks .linkcard .title { color: var(--liquid-accent); font-weight: 600; display: block; overflow-wrap: anywhere; }
.blocks .linkcard .site { color: var(--liquid-secondary-label); font-size: 13px; display: block; margin-top: 2px; }

/* ── A LINK WITH A PICTURE BEHIND IT ────────────────────────────────────────
 *
 * The bordered .linkcard above is what a link drew EVERYWHERE until now,
 * including a YouTube update whose entire subject is a video: a row of text
 * reading "Watch on youtube.com", on the card and again on the page the reader
 * opened to see the thing. See lib-link-preview.go for where the still was
 * hiding and partial-ping-blocks.html for what replaced the row.
 *
 * THE BOX IS AN ASPECT RATIO AND NOT A HEIGHT, and that is the rule the whole
 * frame rests on: 16/9 with the image absolutely filling it means the frame is
 * the same object whether the bytes arrive, arrive late, or never arrive at
 * all. A deleted or region-blocked video is still a video that was here, and a
 * frame that collapses to nothing when its <img> 404s is exactly the failure
 * the app's own error builder exists to stop.
 *
 * NO PLAY DISC. The app draws one only on the update's own screen — the one
 * place a tap honestly leaves for the video — and takes it off a card, where
 * the same tap opens the update. This markup is shared by both surfaces, so it
 * carries the caption, which is true in both places, and not the disc, which
 * would be a promise one of them does not keep. */
.linkpreview { margin: 0 0 16px; }

.linkpreview__frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 14px;
  /* The outline is painted OVER the picture rather than under it. A cover-fit
   * image fills the box to its own edge, so a border under it is a border the
   * image sits on top of and quietly erases at the corners. */
  box-shadow: inset 0 0 0 1px var(--liquid-content-border);
  background: var(--liquid-fill-quiet);
  text-decoration: none;
}

.linkpreview__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE PROVIDER, IN THE CORNER. Dark-on-light is not an option over somebody
 * else's video still — the frame is whatever colour the creator's thumbnail is
 * — so this is its own opaque chip rather than a token that follows the
 * theme. */
.linkpreview__badge {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 3px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.66);
  color: #FFFFFF;
  font-size: var(--type-caption);
  font-weight: 600;
  line-height: 1.2;
}

/* "Watch on youtube.com" — the destination, spelled out, because this is where
 * somebody decides to leave the site for a link. Same reasoning, same gradient
 * problem as the badge: it is over a photograph. */
.linkpreview__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 18px 12px 8px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
  color: #FFFFFF;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE CREATOR'S OWN TITLE, under the frame, where a video site captions a
 * video. Two lines: it is a title, and one that ran on would be giving a
 * video's name more room than the update it belongs to. */
.linkpreview__title {
  margin: 10px 0 0;
  font-size: var(--type-body);
  font-weight: 600;
  line-height: 1.35;
  color: var(--liquid-primary-label);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* THE COMPACT FRAME — the card's copy, with no caption under it.
 *
 * The app's PingLinkPreview(compact: true). A card has already spent its text
 * budget on the body above the picture; the "Watch on {host}" strip and the
 * video's title belong on the update's own page, where there is room to read
 * them. Nothing here removes an element — the template simply does not draw
 * them — so this rule is only the spacing that changes with them gone. */
.linkpreview--compact { margin: 12px 0 0; }

/* The video's own title, under the frame. 13px rather than the body's size —
 * the app's note: "a source line the same size as the body competes with it,
 * and the weight is what keeps it a title at that size". Two lines, because a
 * card that let one run on would give a video's name more room than the update
 * it belongs to. */
.linkpreview__caption-under {
  margin: 8px 0 0;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--liquid-primary-label);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.blocks .more-photos { color: var(--liquid-secondary-label); font-size: 14px; font-style: italic; }

/* ── THE CARD'S BODY, CAPPED AT SIX LINES ───────────────────────────────────
 *
 * See the card-body partial for why the toggle is a <details> and why it is
 * drawn only when Go is CERTAIN the text overflows.
 *
 * The clamp is on the paragraph and the open state is on a LATER SIBLING, which
 * is why this needs :has(): a <details> cannot contain the text, because a
 * closed <details> hides its contents and the whole point of a clamp is that six
 * lines stay visible. .nav already depends on :has() for its create sheet, so
 * the support question is settled in this sheet; a browser without it shows the
 * body unclamped with a toggle that does nothing visible, which is a cosmetic
 * failure rather than a lost paragraph. */
.pbody { margin: 0; }
.pbody__text {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.pbody--clamped .pbody__text {
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.pbody--clamped:has(.pbody__disc[open]) .pbody__text {
  display: block;
  overflow: visible;
}

/* The disclosure itself. `display: contents` on nothing here — the <details> is
 * a real box so the summary has somewhere to sit, but it holds no content, so
 * it is only ever as tall as its own one line. */
.pbody__disc { margin-top: 4px; }
.pbody__toggle {
  display: inline-block;
  cursor: pointer;
  font-size: var(--type-small);
  font-weight: 600;
  color: var(--liquid-accent);
  /* The UA marker is a triangle, which on a text control reads as a tree node.
   * Both spellings, because Safari still wants the -webkit- pseudo-element. */
  list-style: none;
}
.pbody__toggle::-webkit-details-marker { display: none; }
.pbody__toggle:focus-visible {
  outline: 2px solid var(--liquid-accent);
  outline-offset: 2px;
  border-radius: 6px;
}
/* One word is in the document and CSS picks it, so the control changes state
 * with no round trip and no script. */
.pbody__disc:not([open]) .pbody__less { display: none; }
.pbody__disc[open] .pbody__more { display: none; }

/* ── THE CARD'S ONE THUMBNAIL ───────────────────────────────────────────────
 *
 * AN ASPECT RATIO AND NOT A HEIGHT, for the reason .linkpreview__frame gives:
 * the box is what survives when the picture 404s. A private upload whose token
 * has expired, or a bucket object that went away, then leaves a quiet empty
 * frame in the card rather than collapsing the card by 200px and reflowing
 * everything below it mid-scroll.
 *
 * 16/9 rather than the picture's own shape, because the shape is not stored for
 * the derivative — resolveCardMedia draws the THUMB, and only the full image
 * carries width and height. A fixed frame with object-fit: cover is the same
 * answer PingMedia gives in the app: one consistent block per card, whatever
 * was uploaded into it.
 *
 * cover CROPS, which is right here and would be wrong on the update's own page.
 * A card is a glance; the page is where the whole photograph is. */
.pcard__thumb { margin-top: 12px; }
.pcard__thumbimg {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 14px;
  box-shadow: inset 0 0 0 1px var(--liquid-content-border);
  background: var(--liquid-fill-quiet);
}

/* A row of verbs. */
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 var(--space-section); }
.row--actions { flex-wrap: wrap; gap: 10px; }

.label {
  display: inline-block;
  background: var(--liquid-fill-quiet);
  border-radius: var(--liquid-radius-capsule);
  padding: 6px 14px;
  font-size: 14px;
  margin-bottom: 16px;
}

/* The reaction row. The summary pill is drawn for everybody — a blocked tap
 * explains itself rather than the control being absent — so this renders
 * whether or not the viewer may use it. */

/* ═══════════════════════════════════════════════════════════════════════════
 * PHASE ONE — the write surfaces.
 *
 * Everything below styles markup that did not exist before the site could
 * publish, edit, manage circles or change who may reach you. The rules are
 * grouped by what they serve rather than alphabetically, because the reason a
 * value was chosen is usually about the control next to it.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── forms: refusals ────────────────────────────────────────────────────────
 *
 * .formerror is "this was refused": red, and it carries role="alert" so it
 * interrupts.
 *
 * The red is --liquid-destructive-*, which is the same red as a destructive
 * confirm and a field error in the app. One red, three places. */
.formerror {
  margin: 0 0 12px;
  padding: 10px 14px;
  border-radius: var(--liquid-radius-row);
  font-size: 14px;
  line-height: 1.4;
  background: var(--liquid-destructive-surface);
  color: var(--liquid-destructive-label);
  border: var(--liquid-hairline) solid var(--liquid-destructive-rim);
}

/* ── RETIRED 2026-09-24 — .formnotice ──────────────────────────────────────
 *
 * The neutral "this happened" box beside .formerror, with role="status". Its
 * last page, the email list, draws its outcome as a .pnotice line since the
 * 09-24 redesign. It was the fill-quiet surface, the secondary label and a
 * separator hairline, in .formerror's box. */

/* ── a fieldset inside a group ──────────────────────────────────────────────
 *
 * The visibility picker and the trigger picker are <fieldset>, because a set of
 * radios or checkboxes that answer one question needs a <legend> for a screen
 * reader to announce what the question was.
 *
 * The browser's default fieldset border and padding are removed rather than
 * restyled: the group around it already draws the boundary, and two nested
 * boxes read as two sections. */
.group__nested {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0; /* fieldset defaults to min-content, which breaks flex children */
}

.group__nested > .group__header {
  padding-left: 0;
  padding-right: 0;
}

/* ── a text input with a fixed prefix ───────────────────────────────────────
 *
 * The @ in front of a handle. It is NOT part of the value — IsValidHandle
 * refuses a leading @ — so it is drawn beside the field rather than typed into
 * it, and marked aria-hidden because "at" read aloud before every handle is
 * noise.
 *
 * The wrapper carries the .input treatment and the inner field gives up its
 * own, so the two look like one control. */
.input--prefixed {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 0 10px;
}

.input__prefix {
  color: var(--liquid-secondary-label);
  flex: 0 0 auto;
}

.input--prefixed > input {
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 8px 0;
  min-width: 0;
  flex: 1 1 auto;
}

.input--prefixed > input:focus {
  outline: none;
}

/* ── a breadcrumb back to the parent ────────────────────────────────────────
 *
 * One link, on the channel post page, back to its channel. Not a full breadcrumb trail:
 * the site is two levels deep everywhere, and a trail of two is a link. */
.crumbs {
  margin: 0 0 10px;
  font-size: 13px;
}

.crumbs a {
  color: var(--liquid-accent);
  text-decoration: none;
}

/* ── RETIRED — .row--muted ──────────────────────────────────────────────────
 *
 *   .row--muted { opacity: 0.62; }
 *
 * A PAUSED PING ON /pings, when that list was rows. It is cards now, and the
 * pause is the status indicator at the head of one — a glyph with the state's
 * own ARB string as its label, which is a mark a screen reader can read and an
 * opacity is not. See .acard__status.
 *
 * The rule it replaced was right about one thing worth keeping: the state was
 * said in WORDS as well, and the fade was never the only signal. That property
 * survives — the label is the words now, and the glyph is the shape. */

/* ── an edited channel post ─────────────────────────────────────────────────────────
 *
 * Beside the timestamp, in the secondary ink. A separate element rather than
 * appended text so it can be found by a test and read as its own phrase. */
.when__edited {
  margin-left: 6px;
  color: var(--liquid-secondary-label);
}

/* ── RETIRED — .reactions, .reactions--picker, .reactions__count, .chip--active
 *
 * The permanent row of six emoji under every reactable thing, and the read-only
 * tally row above it. Nothing emits either class any more.
 *
 * They went with the app's own decision, which this site had not followed: a
 * strip laid out permanently under a message only works while the vocabulary is
 * five items long, and it was spending a row of screen on controls that were
 * mostly not going to be pressed. The strip now rides an OPENED surface — a
 * card's menu, or the panel behind its pill — and the tallies live inside that
 * panel rather than on the card.
 *
 * See .strip, .pill-react and .cardmenu below, and partial-reactions.html for
 * the rules the shape is implementing. */

/* ── forms that are laid out as rows ────────────────────────────────────────
 *
 * Several of the new screens put a whole <form> where a .row would go — one
 * control per row, publishing on its own. The form has to lay out like the rows
 * around it or the list develops a step at every control.
 *
 * form.row and form.row--actions rather than a wrapper element, because a
 * <div class="row"> containing a form would make the row's own padding fight
 * the form's block layout. */
form.row,
form.row--actions {
  width: 100%;
}

/* Buttons sitting directly in an actions row wrap rather than overflowing.
 * Several rows now carry three — pause, edit, delete — and a narrow phone at a
 * large text scale cannot fit them on one line. */
.row--actions {
  flex-wrap: wrap;
  gap: 8px;
}

/* ── selects match the text inputs ──────────────────────────────────────────
 *
 * The pickers (audience, visibility, timezone, place, person) are <select>, and
 * a browser's default select does not inherit the .input treatment on its own.
 * appearance:none would also remove the disclosure arrow, which is the one
 * affordance saying it opens — so it is deliberately left alone. */
select.input {
  font: inherit;
  color: inherit;
}

/* ── a disabled control says so ─────────────────────────────────────────────
 *
 * The composer's submit when a channel may not publish, and the message editor on
 * a completed ping. Both are courtesies rather than gates — the server refuses
 * either way — so they have to LOOK unavailable or they read as broken. */
.btn[disabled],
.input[disabled],
textarea[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ══ CHANNEL POST CARDS ═══════════════════════════════════════════════════
 *
 * The app's shape, in CSS. See partial-channel-post-card.html for what each part is and
 * why the header carries the time.
 *
 * A card rather than the bordered .channel post row that was here: on a timeline mixing
 * channels, a row gives the reader no way to tell whose channel post they are reading and
 * nowhere to hang a response. .channel post survives below because the PUBLIC pages
 * still use it — those have one channel and one channel post each, where a row is right. */

.pcard {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 24px;
  padding: 16px 16px 24px;
  background: var(--liquid-content-surface);
  /* NEUTRAL AND VISIBLE IN THE READ STATE, where it was `solid transparent`.
   * A transparent border reserved the pixel and painted nothing, so a read channel post
   * was an edgeless pale rectangle on a page four points darker than it —
   * the same bug .card and .group had, and the reason nothing on this site read
   * as a card. The app's channel post card is outlined in liquidContentBorder whatever
   * its read state; --liquid-content-border is that value. */
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card);
}

/* ── RETIRED — .pcard--unread ───────────────────────────────────────────────
 *
 *   .pcard--unread { border-color: var(--liquid-channel-accent); }
 *
 * AN ACCENT BORDER WAS THE THIRD PER-CARD UNREAD MARK, and the app retired all
 * three — a rail, a dot, then this — for one reason: each asks the reader to
 * notice a difference between two otherwise identical cards, once per row,
 * forever, on the longest scroll in the product. See NewChannelPostsDivider in
 * lib/widgets/shared/new_channel_posts_divider.dart, which is where that argument is
 * written out.
 *
 * THE BORDER IS NEUTRAL IN BOTH STATES NOW, which is what liquidContentBorder
 * does in the app and what .pcard above already sets. Nothing overrides it.
 *
 * What replaced it is .newposts — one rule across the list — and .pcard--read
 * below, which steps the text back on everything the reader has been past.
 */

/* READ IS THE MARKED STATE, and that is the way round it has to be.
 *
 * A list is mostly things you have already seen, so this is one quiet treatment
 * applied to the majority — where marking the minority is a loud treatment
 * applied to whatever happens to be new today. It also degrades in the right
 * direction: a mixed timeline can put a read channel post below the rule, and a dim card
 * there is honest where a missing accent would look like a bug.
 *
 * OPACITY ON THE TEXT AND NOT ON THE CARD. Fading the whole element takes the
 * border, the avatar and the reaction pill with it, and a card at 70% on a
 * translucent surface reads as half-loaded rather than as read. The app steps
 * back the LABEL colours for the same reason.
 *
 * The menu button and the chips are excluded: an already-read channel post is not a
 * less interactive one, and a ⋯ that dims is a control that looks disabled. */
.pcard--read .blocks,
.pcard--read .pcard__name,
.pcard--read .pcard__handle {
  color: var(--liquid-secondary-label);
}

/* ── THE "NEW CHANNEL POSTS" RULE ───────────────────────────────────────────────────
 *
 * A line across the list with two words sitting in it, immediately above the
 * first card the reader has already seen. See the note in
 * partial-channel-post-card.html for what it does and does not claim.
 *
 * THE RULE IS A BORDER ON THE PSEUDO-ELEMENTS EITHER SIDE rather than a
 * background gradient behind the text: the label has to sit ON the line with
 * the page showing through, and a gradient would need to know the page colour
 * in both themes. Two flexed hairlines need to know nothing.
 *
 * SECONDARY, NOT AN ACCENT. The line is about the READER's position, not about
 * any one channel, and painting it in the channel orange would make it look like it
 * belonged to whichever card it happened to land above — which is the app's own
 * default for exactly this reason. */
.newupdates {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 4px 0 24px;
  color: var(--liquid-secondary-label);
  font-size: var(--type-small);
  font-weight: 600;
}
.newupdates::before,
.newupdates::after {
  content: "";
  flex: 1 1 auto;
  border-top: 1px solid var(--liquid-separator);
}
.newupdates__label { flex: 0 0 auto; }

.pcard__head { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }

/* The attribution takes the space and the time and menu sit tight to the right,
 * so a long channel name truncates rather than pushing the timestamp off. */
.pcard__channel {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  flex: 1;
  color: inherit;
  text-decoration: none;
}
.pcard__channel:hover .pcard__name { text-decoration: underline; }

/* A ROUNDED SQUARE, not a circle, for the reason the discover card gives: a
 * circle reads as a person and a channel is a publication. Smaller than the
 * catalogue's 52px because this one is a byline rather than an identity block. */
.pcard__avatar {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  color: #FFFFFF;
  font-size: 16px;
  font-weight: 700;
}

/* The eight-colour palette, keyed on the channel id by the same arithmetic the app
 * uses. The hexes match .card__avatar--tN exactly and deliberately: a channel has
 * to be the same colour wherever it appears, and two lists that merely look
 * alike is how that stops being true. */
.pcard__avatar--t0 { background: #B85400; }
.pcard__avatar--t1 { background: #4F46E5; }
.pcard__avatar--t2 { background: #C026D3; }
.pcard__avatar--t3 { background: #E11D48; }
.pcard__avatar--t4 { background: #D97706; }
.pcard__avatar--t5 { background: #0284C7; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pcard__avatar--t0 { background: #FFA24FD9; }
  :root:not([data-theme="light"]) .pcard__avatar--t1 { background: #7F86F0D9; }
  :root:not([data-theme="light"]) .pcard__avatar--t2 { background: #DC6BE8D9; }
  :root:not([data-theme="light"]) .pcard__avatar--t3 { background: #F2617FD9; }
  :root:not([data-theme="light"]) .pcard__avatar--t4 { background: #EFA83AD9; }
  :root:not([data-theme="light"]) .pcard__avatar--t5 { background: #4BA6E0D9; }
}
:root[data-theme="dark"] .pcard__avatar--t0 { background: #FFA24FD9; }
:root[data-theme="dark"] .pcard__avatar--t1 { background: #7F86F0D9; }
:root[data-theme="dark"] .pcard__avatar--t2 { background: #DC6BE8D9; }
:root[data-theme="dark"] .pcard__avatar--t3 { background: #F2617FD9; }
:root[data-theme="dark"] .pcard__avatar--t4 { background: #EFA83AD9; }
:root[data-theme="dark"] .pcard__avatar--t5 { background: #4BA6E0D9; }

.pcard__id { min-width: 0; display: flex; flex-direction: column; gap: 1px; }

.pcard__name {
  font-size: var(--type-body);
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pcard__handle {
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE TIME AND THE ⋯ SIT HARD RIGHT, WITH OR WITHOUT AN ATTRIBUTION.
 *
 * margin-left:auto and not a justify-content on the head, because the head has
 * two arrangements and only one of them was working. With the attribution
 * drawn — the cross-channel timeline — .pcard__channel takes flex:1, the free
 * space is already spent and this auto margin resolves to nothing. WITHOUT it
 * — every card on a channel's own page, which is where a channel is actually
 * read — there was no flex:1 sibling at all, so the timestamp and the menu
 * bunched against the LEFT edge under nothing, and the ⋯ was the furthest
 * thing on the card from where a reader reaches for it.
 *
 * The app right-aligns the time on the header line in both cases and always
 * has; this is the same rule, finally applied to the second case. */
.pcard__when {
  flex: 0 0 auto;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--liquid-secondary-label);
  font-size: 13px;
  white-space: nowrap;
}
.pcard__when a { color: inherit; text-decoration: none; }
.pcard__when a:hover { text-decoration: underline; }

/* ── RETIRED — .pcard__title ────────────────────────────────────────────────
 *
 * The channel post heading, which a card drew above its body. Migration 082 dropped
 * channel_posts.title: a channel post is a block of words and whatever is attached to
 * them, and the header line already names the channel, which is what a reader
 * recognises. The dispatcher had never written a title, so most cards were
 * already drawing nothing here.
 *
 * The read-state rule above lost its .pcard--read .pcard__title selector with
 * it; .blocks and the two name classes still carry the dimming. */

/* THE EDGE. Facts left, response right — the app hangs both on the card's
 * bottom edge, and the extra bottom padding above is what holds it open.
 *
 * pointer-events is off on the container and back on for the two ends, so the
 * gap between them does not swallow clicks meant for the card behind it. */
.pcard__edge {
  position: absolute;
  /* The card's own content inset, so a chip starts under the avatar's left edge
   * rather than floating in from the corner — CardEdgeChips.inset. It tracks
   * .pcard's horizontal padding and moved with it. */
  left: 16px;
  right: 16px;
  bottom: -16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  pointer-events: none;
}
.pcard__edge-left,
.pcard__edge-right { display: flex; align-items: center; gap: 6px; pointer-events: auto; }

/* AMBER SAYS CREDITS, everywhere in the app, and it still does here. The
 * capsule is the only surface; the amber lives in the number. */
.chip--cost { color: #8F6500; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chip--cost { color: #F4C542; }
}
:root[data-theme="dark"] .chip--cost { color: #F4C542; }

/* ══ THE REACTION PILL AND ITS PANEL ═══════════════════════════════════════
 *
 * ONE summary pill on an edge, the breakdown inside what it opens. See
 * partial-reactions.html for the rules; this is only their geometry. */

.react { position: relative; }

/* No marker: a summary draws a disclosure triangle by default and this is a
 * pill, not a section heading. Both properties are needed — the standard one
 * and the WebKit pseudo-element. */
.react > summary { list-style: none; cursor: pointer; }
.react > summary::-webkit-details-marker { display: none; }

.pill-react {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 24px;
  padding: 0 9px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-flat-chrome-fill);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  color: var(--liquid-secondary-label);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The viewer's own reaction tints the WHOLE pill. A pill has no chip to tint,
 * which is less precise than the per-emoji row it replaced and still answers
 * the question a reader actually asks — did I — without them hunting for their
 * glyph among four. */
.pill-react--mine { border-color: #B85400; color: #B85400; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pill-react--mine { border-color: #FFA24F; color: #FFA24F; }
}
:root[data-theme="dark"] .pill-react--mine { border-color: #FFA24F; color: #FFA24F; }

.pill-react__glyphs { display: inline-flex; gap: 1px; font-size: 13px; line-height: 1; }
.pill-react__n { font-size: 12px; }

/* THE FLOATING PANEL. Right-anchored because the pill and the menu button both
 * live on a right edge, and a left-anchored panel there hangs off the viewport
 * on a phone. z-index above the next card so it is not clipped by it. */
.react__panel,
.cardmenu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 20;
  min-width: 220px;
  max-width: min(320px, calc(100vw - 32px));
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-group);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
}

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

/* ══ THE QUICK STRIP ═══════════════════════════════════════════════════════
 *
 * Six glyphs and a way to the rest, on whatever opened surface is showing it.
 * Never permanently on a card — see the partial. */
.strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  padding: 0;
  margin: 0;
}

.strip__glyph {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  border: var(--liquid-hairline) solid transparent;
  background: transparent;
  border-radius: var(--liquid-radius-capsule);
  padding: 5px 7px;
  font-size: 19px;
  line-height: 1;
}
.strip__glyph:hover { background: var(--liquid-fill-quiet); }
.strip__glyph:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 1px; }

/* RINGED, NOT HIDDEN. The one you hold has to stay pressable, because pressing
 * it is the withdrawal. */
.strip__glyph--mine { border-color: #B85400; background: var(--liquid-fill-quiet); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .strip__glyph--mine { border-color: #FFA24F; }
}
:root[data-theme="dark"] .strip__glyph--mine { border-color: #FFA24F; }

.strip__more {
  margin-left: 2px;
  padding: 5px 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--liquid-accent);
  text-decoration: none;
  white-space: nowrap;
}
.strip__more:hover { text-decoration: underline; }

/* ══ THE CARD MENU ═════════════════════════════════════════════════════════
 *
 * The browser's answer to press and hold: a control you can SEE, because
 * neither right-click nor long-press is discoverable. Both gestures open this
 * same element — see the enhancement in layout.html. */
.cardmenu { position: relative; flex: 0 0 auto; }
.cardmenu > summary { list-style: none; cursor: pointer; }
.cardmenu > summary::-webkit-details-marker { display: none; }

/* THE BUTTON IS A SMALL ROUND ⋮, AND IT SHOWS ONLY WHEN IT CAN BE USED — 2026-09-22.
 *
 * The app has no button at all: a card's actions are a press and hold. The
 * web keeps one for the mouse, because right-click is undiscoverable, but it
 * was a pale ⋯ on every card, all the time, which the app never draws. So:
 *
 *   mouse     hidden until the card is hovered or the button has focus, then
 *             a bold ⋮ on a quiet round plate
 *   touch     never drawn; a press and hold opens the menu (layout.html),
 *             exactly as in the app
 *
 * OPACITY, NOT display, ON A POINTER DEVICE, so the button keeps its place and
 * a keyboard can still tab to it (focus-within lights it). The pointer is
 * over the card whenever it could reach the button, so an invisible button is
 * never clicked by accident. On touch it is display: none only inside a [data-card-menu]
 * card, because that is where the hold works; a menu outside one keeps its
 * button rather than becoming unreachable. */
.cardmenu__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--liquid-fill-quiet);
  color: var(--liquid-primary-label);
}
.cardmenu__glyph { width: 18px; height: 18px; }
.cardmenu__btn:hover,
.cardmenu[open] > .cardmenu__btn { background: var(--liquid-separator); }
.cardmenu__btn:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }

@media (hover: hover) and (pointer: fine) {
  [data-card-menu] .cardmenu:not([open]) > .cardmenu__btn { opacity: 0; transition: opacity 0.12s ease; }
  [data-card-menu]:hover .cardmenu > .cardmenu__btn,
  [data-card-menu]:focus-within .cardmenu > .cardmenu__btn { opacity: 1; }
}
@media (hover: none) {
  [data-card-menu] .cardmenu > .cardmenu__btn { display: none; }
  /* The hold is ours, not the browser's: no callout, no text selection. */
  [data-card-menu] { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}

.cardmenu__item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--liquid-radius-row);
  background: transparent;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 14px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.cardmenu__item:hover { background: var(--liquid-fill-quiet); }
.cardmenu__item--destructive { color: var(--liquid-destructive-label); }

/* ── THE MENU ON A ROW, AND THE MENU ON A CARD ──────────────────────────────
 *
 * .cardmenu lives INSIDE the channel post card's header, where it is a flex sibling of
 * the timestamp and needs no placement of its own. On a row and on a channel card
 * it cannot: both of those are a single <a> covering the whole surface, and a
 * <details> inside an anchor is invalid HTML that the browser repairs by
 * closing the anchor early — leaving an element where half the surface silently
 * is not the link it appears to be.
 *
 * So on those the menu is the anchor's SIBLING inside a positioned wrapper, and
 * this places it on the trailing edge over the link.
 *
 * ── RETIRED — .rowwrap ─────────────────────────────────────────────────────
 * There were two wrappers, differing only in where the button sat: .rowwrap for
 * a 44px row and .cardwrap for a card. .rowwrap's last user was
 * page-channels-manage.html, whose two lists are the Following and Publishing
 * segments of /channels since 2026-09-20, so nothing wraps a row any more. Its
 * rules went with it:
 *
 *	.rowwrap { position: relative }
 *	.rowwrap > .cardmenu { position: absolute; top: 50%; right: 10px;
 *	                       transform: translateY(-50%) }
 *	.rowwrap > .row { padding-right: 48px }
 *	.rowwrap > .cardmenu > .cardmenu__btn { opacity: .55 }   (+ hover, focus)
 *	.rowwrap:has(.cardmenu[open]) { z-index: 30 }
 *
 * and so did the three extra hairline selectors it forced — see the note above
 * `.row + .row::before`, which has to be restored WITH this if a row menu ever
 * returns.
 *
 * THE WRAPPER IS NOT THE ROW. .row keeps its own padding, its own hover and its
 * own hairline — nothing about the list changes — and the wrapper adds only a
 * positioning context. Making .row itself the flex parent would have put the
 * button inside the link again, which is the bug this avoids. */
.cardwrap,
.acardwrap { position: relative; }

/* The row's own right padding is 16px and the button is 28 wide, so 10 leaves
 * it clear of the chevron a .row--link draws and clear of the rounded corner a
 * first or last row wears. */
.cardwrap > .cardmenu,
/* A ping card is the third of these and takes the card placement rather than
 * the row one: it is a block with a 16px inset, not a 44px line. Sharing the
 * rule rather than restating it is what stops the ⋯ sitting two pixels off on
 * one of the three surfaces that draw it. */
.acardwrap > .cardmenu { position: absolute; top: 14px; right: 14px; }

/* ROOM FOR THE BUTTON, so a long title runs under the ⋯ rather than into it.
 * On the card this is padding-right on the identity block alone: the
 * description below wraps at the full width, and reserving the gutter for all
 * of it would leave a ragged column beside a control that is only at the top. */
.cardwrap > .card > .card__head { padding-right: 34px; }

/* THE BUTTON IS QUIETER OVER A LINK THAN IT IS ON A CARD HEADER, because here
 * it sits ON something clickable and a solid control would read as the row's
 * primary action. It comes up to full strength on hover and on focus, and
 * whenever the menu is open.
 *
 * OPACITY RATHER THAN A THIRD LABEL COLOUR: there is no --liquid-tertiary-label
 * and inventing one for this would mean a value in both themes plus the two
 * theme-override blocks, for a control that only needs to recede. */
/* RETIRED 2026-09-22 — the 0.55 opacity over a link. The button is hidden until
 * the card is hovered now, everywhere; see the card menu block above. */

/* The open panel has to sit above the rows and cards after it, and .group
 * clips its children — see the overflow note there — so a row menu inside a
 * grouped panel would be cut off at the panel's edge without this. */
.group:has(.cardmenu[open]) { overflow: visible; }
.cardwrap:has(.cardmenu[open]),
.acardwrap:has(.cardmenu[open]) { z-index: 30; }

.cardmenu__note {
  margin: 0;
  padding: 2px 10px 4px;
  color: var(--liquid-secondary-label);
  font-size: 12px;
}

/* ══ THE FULL EMOJI PICKER ═════════════════════════════════════════════════
 *
 * Its own page, for the reason the strip gives: nine hundred buttons per card
 * is not a panel a browser can afford. */
.picker { display: flex; flex-direction: column; gap: 10px; }

.picker__cat {
  background: var(--liquid-content-surface);
  border-radius: var(--liquid-radius-group);
  padding: 10px 12px;
}

.picker__catname {
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--liquid-secondary-label);
}

/* auto-fill rather than a fixed count, so the grid is dense on a desktop and
 * still holds 40px targets on a phone. */
.picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
  gap: 2px;
  margin-top: 8px;
}

.picker__glyph {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  border: var(--liquid-hairline) solid transparent;
  background: transparent;
  border-radius: 8px;
  padding: 6px 0;
  font-size: 21px;
  line-height: 1.2;
}
.picker__glyph:hover { background: var(--liquid-fill-quiet); }
.picker__glyph:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 1px; }
.picker__glyph--mine { border-color: #B85400; background: var(--liquid-fill-quiet); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .picker__glyph--mine { border-color: #FFA24F; }
}
:root[data-theme="dark"] .picker__glyph--mine { border-color: #FFA24F; }

/* ══ CREDITS AND THE PLAN ══════════════════════════════════════════════════ */

/* _BalanceHero — credits_screen.dart (2026-09-24). A centred card in the credit
 * colour (Palette.credit): the mark, the total, the label, the two buckets. */
.balhero {
  --credit: #8F6500;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 0 24px;
  padding: 24px;
  background: var(--liquid-content-surface);
  border: 1px solid color-mix(in srgb, var(--credit) 30%, transparent);
  border-radius: var(--liquid-radius-card, 20px);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .balhero,
  :root:not([data-theme="light"]) .packnote { --credit: #F4C542; }
}
:root[data-theme="dark"] .balhero,
:root[data-theme="dark"] .packnote { --credit: #F4C542; }
.balhero__mark,
.packnote__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--credit) 15%, transparent);
  color: var(--credit);
}
.balhero__mark .nav__glyph,
.packnote__mark .nav__glyph { width: 30px; height: 30px; }
.balhero__n { margin-top: 14px; font-size: 44px; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.balhero__label { margin-top: 4px; font-size: 13px; font-weight: 600; letter-spacing: 0.3px; color: var(--liquid-secondary-label); }
.balhero__buckets { display: flex; gap: 10px; width: 100%; margin-top: 16px; }
.bucket {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid var(--liquid-content-border);
  background: var(--liquid-fill-quiet);
}
.bucket__n { display: flex; align-items: center; gap: 6px; font-size: 20px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.bucket__n .nav__glyph { width: 14px; height: 14px; color: var(--credit); }
.bucket__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 500; color: var(--liquid-secondary-label); }
.balhero__note { margin: 12px 0 0; text-align: center; font-size: 13px; line-height: 1.45; font-weight: 500; color: var(--liquid-secondary-label); }

/* Where the app lists packs: its empty state, an icon over a sentence. */
.packnote {
  --credit: #8F6500;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin: 0 0 24px;
  padding: 20px;
  text-align: center;
}
.packnote__text { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.4; color: var(--liquid-secondary-label); }

/* _CreditRatesCard. Free in the brand colour, a price in the credit colour,
 * the two coming-soon lines muted. */
.ratecard {
  margin: 0 0 24px;
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
}
.ratecard__head { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 15px; font-weight: 700; }
.ratecard__head .nav__glyph { width: 18px; height: 18px; color: #9E6F00; }
.ratecard__sub { margin: 6px 0 12px; font-size: 13px; line-height: 1.3; color: var(--liquid-secondary-label); }
.ratecard__list { list-style: none; margin: 0; padding: 0; }
.ratecard__row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; }
.ratecard__row .nav__glyph { width: 16px; height: 16px; flex: 0 0 auto; color: var(--liquid-secondary-label); }
.ratecard__label { flex: 1; min-width: 0; }
.ratecard { --cost: #8F6500; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ratecard { --cost: #F4C542; }
}
:root[data-theme="dark"] .ratecard { --cost: #F4C542; }
.ratecard__cost { font-weight: 700; color: var(--cost); }
.ratecard__cost--free { color: var(--liquid-accent); }
.ratecard__row--soon { opacity: 0.55; }
.ratecard__row--soon .ratecard__cost { color: var(--liquid-tertiary-label); }

/* The SIGN carries the meaning and the colour only reinforces it, so a reader
 * who cannot distinguish the two loses nothing. */
.ledger__n { font-variant-numeric: tabular-nums; font-weight: 600; }
.ledger__n--debit { color: var(--liquid-destructive-label); }

/* ── THE PLAN — subscription_screen.dart (2026-09-24) ───────────────────────
 * _HeroHeader, StoreSectionHeader, _IncludedBand, _PlanCardNew and the foot.
 * Cards go two across only where each gets 360px, as responsiveColumnCount
 * decides in the app; the free card keeps its half-width slot beside nothing. */
.subhero,
.subband {
  margin: 0 0 24px;
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
}
.subhero { display: flex; align-items: center; gap: 14px; }
.subhero__mark {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: color-mix(in srgb, #0C8863 20%, transparent);
  border: 1.5px solid color-mix(in srgb, #0C8863 40%, transparent);
  color: var(--liquid-accent);
}
.subhero__mark .nav__glyph { width: 26px; height: 26px; }
.subhero__text,
.subband { display: flex; flex-direction: column; min-width: 0; }
.subhero__title { font-size: 17px; font-weight: 700; }
.subhero__sub { margin-top: 3px; font-size: 13px; line-height: 1.3; color: var(--liquid-secondary-label); }
.subhead { margin: 0 4px 12px; }
.subhead__title { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.2px; }
.subhead__sub { margin: 4px 0 0; font-size: 13px; line-height: 1.3; color: var(--liquid-secondary-label); }
.subband__title { font-size: 15px; font-weight: 700; }
.subband__body { margin-top: 4px; font-size: 13px; line-height: 1.35; color: var(--liquid-secondary-label); }

.plans {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 16px 14px;
  margin: 0 0 24px;
}
@media (max-width: 760px) { .plans { grid-template-columns: 1fr; } }

.plan {
  --accent: #80766C;
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 20px;
  background: var(--liquid-content-surface);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: var(--liquid-radius-card, 20px);
}
/* Every paid tier is premium (PlanBadge.colorFor, 2026-09-28); Free keeps the
 * neutral above. */
.plan--o1 { --accent: #7F61E4; }
.plan--o2 { --accent: #7F61E4; }
.plan--o3 { --accent: #7F61E4; }
.plan--hi { --accent: #0C8863; }

.plan__tab {
  position: absolute;
  top: -12px;
  right: 18px;
  padding: 4px 12px;
  border-radius: 12px;
  background: #0C8863;
  box-shadow: 0 3px 8px color-mix(in srgb, #0C8863 35%, transparent);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.6px;
}
.plan__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.plan__id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.plan__name { font-size: 20px; font-weight: 700; }
.plan__desc { font-size: 13px; color: var(--liquid-secondary-label); }
.plan__current {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-radius: 20px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
}
.plan__status { margin-top: 8px; font-size: 13px; color: var(--liquid-secondary-label); }
.plan__features { list-style: none; margin: 14px 0 0; padding: 0; }
.plan__feature { display: flex; align-items: flex-start; gap: 10px; padding: 4px 0; }
.plan__tick {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--accent);
}
.plan__tick .nav__glyph { width: 13px; height: 13px; }
.plan__line { display: flex; flex-direction: column; font-size: 13px; line-height: 1.35; }
.plan__detail { font-size: 12px; line-height: 1.3; color: var(--liquid-secondary-label); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .plan__tick { background: color-mix(in srgb, var(--accent) 28%, transparent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
}
:root[data-theme="dark"] .plan__tick { background: color-mix(in srgb, var(--accent) 28%, transparent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }

.subtrust {
  margin: 0 auto 24px;
  max-width: 520px;
  text-align: center;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--liquid-secondary-label);
}
.sublegal { display: flex; justify-content: center; gap: 8px; margin: 0 0 24px; font-size: 12px; color: var(--liquid-secondary-label); }

/* One firing on the ping history: a row whose trailing slot holds the pill and
 * the menu rather than a value. */
.row--firing .row__trailing { display: flex; align-items: center; gap: 4px; }

/* THE EDGE PILL OPENS UPWARD, and that is a correctness fix rather than taste.
 *
 * The pill straddles the card's BOTTOM edge, so a panel hanging below it lands
 * on the next card.
 *
 * # THE ORIGINAL REASON HAS EXPIRED AND THE RULE HAS NOT
 *
 * It was a correctness fix: .pcard carried backdrop-filter, a backdrop-filter
 * creates a STACKING CONTEXT, and card two's context therefore painted entirely
 * above card one's — panel included, whatever z-index the panel was given. It
 * looked like random clipping and was the panel being painted under the card
 * below.
 *
 * The glass layer went on 2026-09-06 and took the backdrop-filter with it.
 * .pcard is now `position: relative` with no z-index, which is NOT a stacking
 * context, so a positive z-index on a downward panel would in fact lift it over
 * the next card. The correctness argument no longer applies. It is written down
 * rather than deleted because a rule whose stated reason has quietly stopped
 * being true is the kind that gets "fixed" back by somebody who checks.
 *
 * IT STAYS ANYWAY, on the gesture argument that was always the second half: the
 * panel appears over the thing it belongs to, which is what a context menu does
 * on both phone platforms and what the app's own card menu does. Opening
 * downward would also cover the next channel post's header rather than its own card.
 *
 * Opening upward keeps the panel inside its own card's stacking context, where
 * z-index works normally. It is also the better gesture: the panel appears over
 * the thing it belongs to, which is what a context menu does on both phone
 * platforms.
 *
 * The HEADER menu is unaffected and still opens downward — it starts at the top
 * of the card and covers the card's own body. */
.pcard__edge .react__panel {
  top: auto;
  bottom: calc(100% + 8px);
}

/* A card with something open floats above its neighbours.
 *
 * Belt to the braces above: it covers the header menu on a short card, whose
 * panel can be taller than the card it hangs from. :has() is a progressive
 * enhancement — a browser without it falls back to the panel being clipped by
 * the next card, which is the behaviour that existed before either rule. */
.pcard:has(details[open]) { z-index: 30; }

/* ── RETIRED 2026-09-24 — .daypicker, .daypicker__day ──────────────────────
 *
 * The weekday picker on the old schedule page (partial-schedule.html): seven
 * labelled checkboxes in a seven-column grid, a ticked day filled by
 * :has(input:checked). The schedule page went when the edit form took over
 * the When (page-ping-edit.html), and its day row is the .daybar the
 * composer already drew. */

/* ══════════════════════════════════════════════════════════════════════════
 * THE SIGN-IN PANEL, AND THE DIALOG IT OPENS IN
 *
 * One panel (.authform), two hosts: .authdlg over any page, and .authpage at
 * /signin. Everything visual belongs to the panel; the two host rules below do
 * nothing but position it, which is what stops the two drifting into two
 * different-looking sign-in screens.
 *
 * PHONE FIRST, and not as a slogan: the panel is a full-width sheet rising from
 * the bottom of the window below 700px and a centred card above it, and the
 * base rules here are the sheet. That order matters because the sheet is the
 * harder of the two — safe-area insets, a keyboard that halves the viewport,
 * and a scroll that must stay inside the panel — and writing it as an override
 * of the desktop card is how those three get half-applied.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── the dialog ─────────────────────────────────────────────────────────────
 *
 * The <dialog> box itself is the whole viewport and paints NOTHING. That is
 * what makes "click the backdrop to close" a target test in the script rather
 * than a coordinate calculation: the area around the panel is this element, so
 * a click that lands on it is a click that missed the panel.
 *
 * The browser's own dialog defaults — a centred box, a border, a white fill,
 * `width: fit-content` — are all reset here. Every one of them would fight the
 * sheet on a phone. */
.authdlg {
  /* PINNED ON ALL FOUR SIDES rather than sized. The UA stylesheet gives a modal
   * dialog `position: fixed` with only the BLOCK insets set and a max-width of
   * "the viewport minus its own default border and padding" — numbers this rule
   * removes. Stating the insets is what makes the element the viewport in every
   * engine instead of relying on which halves of that UA rule survive. */
  inset: 0;
  width: 100%;
  max-width: none;
  max-height: none;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--liquid-primary-label);
  /* The sheet sits at the bottom on a phone; the card is centred at width. Both
   * come from this one flex box. */
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* A DIALOG THAT IS NOT OPEN MUST NOT BE A FULL-VIEWPORT BOX SWALLOWING CLICKS.
 *
 * The UA stylesheet hides a closed <dialog> with `display: none`, and the rule
 * above replaces display for every state — so without this the panel would be
 * laid out and visible on every signed-out page, with the sign-in form sitting
 * over the catalogue permanently.
 *
 * !important because the declaration it has to beat is the one directly above
 * it, which is on the same element with the same specificity and comes first
 * only by source order — and [open] cannot be used to scope the other rule,
 * since ::backdrop and the open state are what the browser toggles, not us. */
.authdlg:not([open]) { display: none !important; }

.authdlg::backdrop {
  /* Not a token: custom properties do not inherit into ::backdrop in every
   * engine — it is rendered in the top layer, outside the element tree — so a
   * var() here resolves to nothing in some browsers and the backdrop vanishes.
   * A literal is the honest choice for the one selector that cannot see the
   * palette. */
  background: rgba(31, 27, 22, 0.45);
}

/* THE PANEL INSIDE THE DIALOG IS THE SHEET.
 *
 * Rounded at the top only, flush to the bottom edge, and clear of the home
 * indicator by the safe-area inset — which is the app's own bottom sheet and is
 * the shape somebody reaches for with a thumb.
 *
 * max-height with an internal scroll rather than a height: a keyboard opening
 * on a 667px phone leaves about 300px of visible viewport, and a panel taller
 * than the window with the page scrolling behind it is how a submit button ends
 * up unreachable. dvh and not vh: vh is the LARGEST viewport on mobile Safari,
 * so a vh-sized sheet is permanently taller than the space it actually has. */
.authdlg__panel {
  position: relative;
  width: 100%;
  max-height: 92dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 24px 22px calc(28px + env(safe-area-inset-bottom, 0px));
  /* THE PANEL IS A CARD, SO IT TAKES THE CARD'S FILL — white in light mode,
   * above the page behind it. It was --liquid-grouped-background, which is the
   * PAGE colour: correct while a card and the page were four points apart and
   * plainly wrong now that they are not. A sheet the same colour as what it
   * covers has no edge, and the backdrop was doing all the work of saying that
   * something had opened. */
  background: var(--liquid-content-surface);
  border-radius: var(--liquid-radius-card) var(--liquid-radius-card) 0 0;
  border-top: 1px solid var(--liquid-content-border);
  box-shadow: 0 -8px 34px rgba(31, 27, 22, 0.18);
}

/* The close control. A round chip in the trailing corner rather than a "Cancel"
 * row, because the panel is a sheet and a sheet's own affordances are Escape
 * and the backdrop — this is the visible third one, for a pointer. */
.authdlg__close {
  position: absolute;
  top: 12px;
  inset-inline-end: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  /* The same fill and hairline as .chipbtn in the page head — it is the same
   * object, a round control on a neutral ground. The QUIET fill and not the
   * chrome one: the panel under it is white now, and chrome is white too. */
  background: var(--liquid-fill-quiet);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-secondary-label);
  cursor: pointer;
}
.authdlg__close:hover { color: var(--liquid-primary-label); }
.authdlg__x { width: 18px; height: 18px; stroke-width: 2; }

/* ── the other host ─────────────────────────────────────────────────────────
 *
 * /signin renders the same panel inline. It gets a width and nothing else: the
 * page already has the shell around it and the page head above it, and a second
 * card wrapped around a panel that is already one is the kind of double frame
 * that makes a page look like two designs met in the middle. */
.authpage {
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  padding: 8px 0 32px;
}

/* ── the panel ──────────────────────────────────────────────────────────────
 *
 * A column with one gap, so nothing inside needs a margin of its own. Margins
 * between siblings in a form are how the spacing ends up different in the
 * dialog and on the page — the two hosts would each collapse them differently. */
.authform {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.authform__title {
  margin: 0;
  /* Not --type-display: that token is the page head\x27s large title, and
   * a 40px greeting inside a sheet is a headline arguing with the page behind
   * it. --type-heading grew with the desktop tier on its own — 24px at width,
   * 22 on a phone — which is the whole reason the tier is a token and not a
   * pass over every rule that wanted to be bigger. */
  font-size: var(--type-heading);
  font-weight: 700;
  letter-spacing: -0.02em;
  /* Clear of the close chip, which is positioned over this corner. Padding on
   * the heading rather than a margin on the panel, so /signin — which draws no
   * close chip — is not indented for a control it does not have.
   *
   * CENTRED, as auth_screen.dart centres its title and line — so the clearance
   * is taken on BOTH sides, or the words would sit 20px left of the middle. */
  padding-inline: 40px;
  text-align: center;
}

.authform__sub {
  margin: -8px 0 0;
  text-align: center;
  font-size: var(--type-small);
  color: var(--liquid-secondary-label);
}

/* ── THE SEGMENTED CONTROL ──────────────────────────────────────────────────
 *
 * LiquidSegmentedControl (lib/widgets/liquid/liquid_segmented_control.dart) in
 * CSS, at the top of the auth panel where auth_screen.dart puts it. Values are
 * the widget's own rather than picked to look close:
 *
 *	height 44px            kLiquidSegmentedHeight
 *	inset 3px              _kTrackInset — the gap that makes the thumb read
 *	                       as sitting IN the track rather than over it
 *	radius height/2        both track and thumb; a capsule, not a rounded box
 *	track                  the grouped-surface fill in flat mode
 *	thumb                  opaque white light / white 18% dark
 *	thumb shadow           0 1px 4px black at 8% light, 20% dark
 *	label 15px             w700 selected, w500 not, 0.1px tracking
 *	unselected label       the foreground at 55%
 *
 * # WHY THE TRACK IS --liquid-fill-quiet AND NOT THE APP'S GROUPED SURFACE
 *
 * The app's flat track is liquidGroupedSurface — the row fill — because in the
 * app a card is #F1F5F9 on a #F2F2F7 page and the track is a shade off both.
 * On this site a card is WHITE (see tokens.css), and the panel this control
 * sits in is a card, so the app's value would be a white track holding a white
 * thumb: the control would vanish and only the shadow would remain. This is
 * the same trap --liquid-fill-quiet was introduced for, and it is the token
 * that means "one step off whatever it sits on".
 *
 * # THERE IS NO SLIDING THUMB
 *
 * The app animates the thumb between segments over 320ms with easeOutBack. Two
 * absolutely-positioned halves could do it here, but the thumb would then be a
 * separate element from the label it belongs to, and every state — hover,
 * focus ring, the selected label's weight — would have to be kept in step
 * across two boxes. The background moves instantly instead. A 320ms flourish
 * is not worth an accessibility tree with a decorative div in it. */
.seg {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 0;
  height: 44px;
  padding: 3px;
  margin: 0 0 var(--space-section);
  border-radius: var(--liquid-radius-capsule);
  /* Palette.track, the flat track LiquidSegmentedControl paints since
   * 2026-09-28. It was Palette.inset, 1.03:1 on the linen page: the track has
   * no shadow of its own, so it vanished and only the white thumb showed. */
  background: var(--liquid-track);
}

.seg__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: none;
  /* The halves are anchors, so the link colour and underline this sheet gives
   * every <a> have to be undone — they are a control, not prose.
   *
   * 55% of the label colour is the widget's own unselected alpha
   * (fg.withValues(alpha: 0.55)). The flat declaration first is a fallback for
   * a browser without color-mix; it is the same colour the token resolves to in
   * light mode, so the worst case is a correct light theme and a slightly
   * heavy dark one rather than an invisible label. */
  color: rgba(31, 27, 22, 0.55);
  color: color-mix(in srgb, var(--liquid-primary-label) 55%, transparent);
  text-decoration: none;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.1px;
  cursor: pointer;
  /* Colour and weight only. Transforming or resizing on selection would move
   * the two labels relative to each other every time the mode changed. */
  transition: background-color 160ms ease, color 160ms ease;
}

.seg__btn--on {
  background: #fff;
  color: rgba(0, 0, 0, 0.88);
  font-weight: 700;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

/* THE COUNT ON A SEGMENT, and it is inside the anchor rather than beside it:
 * "Declined" and "4" are one label and one target, and a badge that could be
 * clicked past would be a second, smaller control nobody aimed at.
 *
 * It inherits its colour, so it dims with the unselected label and sharpens
 * with the selected one — the figure belongs to the word, not to the track. */
.seg__badge {
  margin-left: 6px;
  font-size: 13px;
  font-weight: 600;
  /* Tabular figures so a control whose counts change does not reflow its own
   * labels as somebody clears a queue. */
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

.seg__btn:focus-visible {
  outline: 2px solid var(--liquid-accent);
  outline-offset: 1px;
}

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

/* Dark takes the widget's dark pair: a translucent white thumb over the same
 * quiet track, because on a #1E293B panel an opaque white thumb is a hole. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .seg__btn--on {
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.20);
  }
}

:root[data-theme="dark"] .seg__btn--on {
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.20);
}

/* ── the fields ─────────────────────────────────────────────────────────────
 *
 * A tighter gap than the panel's, so the form reads as one block inside it
 * rather than as four more loose rows. */
.authform__form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.authform__label {
  font-size: var(--type-caption);
  font-weight: 600;
  color: var(--liquid-secondary-label);
}

/* .input is `flex: 1` for the search bar it was written for, and inside a
 * COLUMN that means "grow to fill the height". Left alone, the email field
 * would stretch to whatever the panel had spare. */
.authform__form .input { flex: 0 0 auto; width: 100%; }

/* The password field and its reveal toggle share one box, so the button sits
 * INSIDE the field rather than beside it as a second control. */
.authform__pw { position: relative; display: flex; }
.authform__pw .input { padding-inline-end: 46px; }

/* THE PREFIX GLYPH — LiquidTextField's prefixIcon, inside the field at the
 * start, in the tertiary label. The field keeps room for it. */
.authform__field { position: relative; display: flex; }
.authform__field .input { padding-inline-start: 44px; }
.authform__prefix {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  color: var(--liquid-tertiary-label);
}
.authform__prefix .nav__glyph { width: 19px; height: 19px; }

.authform__reveal {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--liquid-secondary-label);
  cursor: pointer;
}
.authform__reveal:hover { color: var(--liquid-primary-label); }
.authform__eye { width: 20px; height: 20px; stroke-width: 1.7; }

/* A field that failed validation. THE RIM ONLY — no fill, no icon: the message
 * under it already says what is wrong, and a red-filled input above a red
 * paragraph is two alarms for one mistake. */
.input--bad { border-color: var(--liquid-destructive-icon); }

.authform__err {
  margin: -2px 0 0;
  font-size: var(--type-caption);
  color: var(--liquid-destructive-icon);
}

/* Trailing-aligned and quiet, where the app puts it. A button and not a link,
 * because it PERFORMS something — it asks Firebase to send mail — and a link
 * that is not a destination is the control people middle-click and then wonder
 * why nothing opened. */
.authform__forgot {
  align-self: flex-end;
  margin: -2px 0 2px;
  padding: 4px 2px;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--type-caption);
  font-weight: 600;
  color: var(--liquid-primary-action);
  cursor: pointer;
}

.authform__submit {
  justify-content: center;
  min-height: 48px;
  font-weight: 600;
}

/* The status line: signing in, creating the account, the reset mail, and the
 * one generic failure. Destructive by default because failure is what it says
 * most often; --ok is the reset mail, the only good news it carries. */
.authform__status {
  margin: 0;
  padding: 10px 12px;
  border-radius: var(--liquid-radius-row);
  font-size: var(--type-small);
  background: var(--liquid-destructive-surface);
  color: var(--liquid-destructive-label);
}
.authform__status--ok {
  /* Same reasoning as the close chip: this sits on the white panel, so the
   * good news needs a fill that steps DOWN from it. The failure state above
   * has its own red surface and needs no such care. */
  background: var(--liquid-fill-quiet);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-primary-label);
}

/* The rule with a word in it. A grid of line/word/line rather than a
 * pseudo-element on each side, so the word's own width is what the two rules
 * subtract and the pair stay equal in every language. */
.authform__or {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
}
.authform__or::before,
.authform__or::after {
  content: "";
  height: 1px;
  background: var(--liquid-separator);
}

/* Google's button. ALWAYS OPAQUE and never glass — the same call the app makes
 * (auth_screen.dart passes glass: false): glass is chrome material, for
 * something floating over content, and this sits inline in a panel beside a
 * plain filled button. */
.authform__google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;
  font-weight: 600;
  color: var(--liquid-primary-label);
}

/* The mark keeps its own four colours — see partial-icons.html for why that is
 * a requirement rather than a preference. */
.authform__gmark { flex: 0 0 auto; width: 20px; height: 20px; }

/* The app's terms line: 12pt tertiary, centred, the two links in the primary
 * label and underlined (auth_screen.dart's _termsAndPrivacy). */
.authform__legal {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  white-space: pre-line;
  color: var(--liquid-tertiary-label);
}
.authform__legal a { color: var(--liquid-primary-label); text-decoration: underline; }

/* Both doors are disabled together while a sign-in is in flight, and the cursor
 * has to say so: .btn:disabled sets the opacity and nothing else. */
/* ── THE PANEL WHILE A SIGN-IN IS RUNNING ───────────────────────────────────
 *
 * Every control in it is disabled by setBusy — the fields and the reset as well
 * as the two doors, for the reasons written out there. This is what that LOOKS
 * like, and the two halves do different jobs:
 *
 *   the cursor      says the page is working rather than broken. A disabled
 *                   control with the ordinary arrow over it reads as a control
 *                   that has stopped responding.
 *   the seg         is two <a> elements, which have no disabled state at all.
 *                   pointer-events is the only thing that stops a mouse press,
 *                   and it is a courtesy: aria-disabled announces it and the
 *                   panel.s own click handler is what actually refuses, because
 *                   a keyboard activation never touches either of these.
 *
 * THE FIELDS ARE NOT DIMMED FURTHER. [disabled] already drops them to 0.5
 * opacity site-wide, and stacking a second fade on the whole panel makes a
 * three-second wait look like a page that failed to load. */
.authform--busy { cursor: progress; }
/* Every disabled control in the panel, not only the two .btn doors: the reveal
 * eye and the reset link are bare <button>s with classes of their own, and they
 * are disabled now too. */
.authform--busy :disabled { cursor: progress; }
.authform__reveal:disabled,
.authform__forgot:disabled { opacity: 0.5; }
.authform--busy .seg { pointer-events: none; opacity: 0.5; }

/* ── AND ABOVE 700px IT IS A CARD ───────────────────────────────────────────
 *
 * The sheet is right where a thumb is and wrong on a desktop, where the pointer
 * is equidistant from everywhere and a panel welded to the bottom edge of a
 * 1400px window is a long way from whatever was clicked. Centred, capped at
 * 420px, rounded on all four corners.
 *
 * SAME BREAKPOINT AS THE NAV, and that is not a coincidence worth losing: 700px
 * is where this site stops being a phone, and a panel that changed shape at its
 * own number would be a second answer to one question. */
@media (min-width: 701px) {
  .authdlg { align-items: center; }

  .authdlg__panel {
    width: min(460px, calc(100vw - 48px));
    max-height: min(90dvh, 760px);
    padding: 28px 26px;
    border: 1px solid var(--liquid-content-border);
    border-radius: var(--liquid-radius-card);
    box-shadow: 0 18px 48px rgba(31, 27, 22, 0.24);
  }

  /* On the page host the shell is already centring the column. */
  .authpage { padding-top: 24px; }
}

/* ── AND AT 700px AND BELOW IT IS A WHOLE SCREEN (2026-10-13) ─────────────────
 *
 * "Web in mobile view shows auth screen popup for signup/in but it should use
 * full screen login auth screen like how mobile app does" (user). The app's
 * sign-in is a page of its own, the form centred on the page colour, and a
 * bottom sheet over a phone's catalogue read as a prompt to dismiss rather
 * than a door. So below the breakpoint the panel fills the window: no
 * rounding, no shadow, no backdrop showing round it, the page's own colour,
 * and the close chip clear of the notch.
 *
 * STILL THE DIALOG. Opening it does not navigate, so closing it is the page
 * the reader was on, and signing in returns there (signInReturnTo) — the two
 * things the dialog was built for, which a link to /signin would give away.
 *
 * Centred with auto margins inside a scrolling column rather than with
 * justify-content: a centred flex child taller than its box is cut off at
 * the TOP, where no scroll can reach it — what a keyboard halving the
 * viewport would do to the title. Auto margins fall to zero instead. */
@media (max-width: 700px) {
  .authdlg__panel {
    display: flex;
    flex-direction: column;
    height: 100%;
    max-height: none;
    padding: calc(64px + env(safe-area-inset-top, 0px)) 22px
      calc(28px + env(safe-area-inset-bottom, 0px));
    background: var(--liquid-grouped-background);
    border-radius: 0;
    border-top: 0;
    box-shadow: none;
  }

  .authdlg__panel > .authform {
    width: 100%;
    max-width: 420px;
    margin: auto;
  }

  .authdlg__close {
    top: calc(12px + env(safe-area-inset-top, 0px));
    inset-inline-end: 16px;
    width: 36px;
    height: 36px;
  }

  /* The app's page push: up a little, and in. */
  .authdlg[open] .authdlg__panel {
    animation: authdlg-rise 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
}

@keyframes authdlg-rise {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .authdlg[open] .authdlg__panel { animation: none; }
}

/* ══ THE PHOTO VIEWER ══════════════════════════════════════════════════════
 *
 * One photograph, filling the window, with the rest of its run one press away.
 * See partial-lightbox.html for what it is and why every line of its script is
 * optional — the tiles are real links, so a browser that never runs it opens
 * the picture instead.
 *
 * # THE ELEMENT DOES THE HARD PARTS
 *
 * showModal() gives the top layer, a focus trap, Escape, inert content behind,
 * and a ::backdrop to paint. None of that is styled here because none of it is
 * ours; what is here is a black ground, a centred picture and three controls.
 *
 * THE DIALOG'S OWN BOX IS RESET TO NOTHING. A <dialog> ships with a white
 * background, a solid border, padding and a centring `margin: auto` that assume
 * a small panel of text. Every one of them is wrong for a full-bleed viewer and
 * all four have to be undone explicitly. */
.lightbox {
  width: 100vw;
  max-width: 100vw;
  height: 100vh;
  max-height: 100vh;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  overflow: hidden;
}

/* Not quite black: a photograph with dark edges against pure black loses its
 * own boundary, and the reader cannot tell where the picture stops. */
.lightbox::backdrop { background: rgba(0, 0, 0, 0.88); }

/* The dialog is only a box when it is open. `display: flex` on the element
 * itself would defeat the [open] semantics the browser applies. */
.lightbox[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* CONTAIN, NEVER COVER. This is the surface the full size exists for, and a
 * viewer that crops is a viewer that hides the part somebody opened it to see.
 * The maxima leave room for the controls at the edges. */
.lightbox__img {
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 120px);
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 8px;
}

/* WHILE THE FULL SIZE LOADS the picture is held invisible — so the previous
 * photograph never shows under the next one's URL — and a white ring sits in
 * the middle of the screen. The script sets and clears .lightbox--loading. */
.lightbox__spinner {
  position: fixed;
  width: 36px;
  height: 36px;
  margin: -18px 0 0 -18px;
  border-width: 3px;
  color: rgba(255, 255, 255, 0.85);
  display: none;
}
.lightbox--loading .lightbox__spinner { display: block; }
.lightbox--loading .lightbox__img { opacity: 0; }

/* THE THREE CONTROLS. A translucent disc over the picture rather than a bar
 * beside it: a bar would take height from the photograph on every screen, and
 * these are pressed once each.
 *
 * 44px, which is the touch floor as a BOX here rather than as a target — they
 * float over the image with nothing beside them to mis-hit. */
.lightbox__btn {
  position: fixed;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: rgba(255, 255, 255, 0.16);
  color: #FFFFFF;
  cursor: pointer;
}
.lightbox__btn:hover { background: rgba(255, 255, 255, 0.28); }

/* DISABLED AT THE ENDS RATHER THAN REMOVED, so the control does not move out
 * from under a pointer that is already on it. A run of ONE hides both instead,
 * which is a different statement: there is no navigation to describe at all. */
.lightbox__btn:disabled { opacity: 0.3; cursor: default; }
.lightbox__btn:disabled:hover { background: rgba(255, 255, 255, 0.16); }

.lightbox__btn--close { top: 16px; right: 16px; }
/* Vertically centred on the window rather than on the picture: a portrait and a
 * panorama would otherwise put the same control in two different places on two
 * consecutive presses of it. */
.lightbox__btn--prev { left: 12px; top: 50%; }
.lightbox__btn--next { right: 12px; top: 50%; }

/* The pips, along the foot. Decoration — the arrows announce the position, and
 * these are aria-hidden. */
.lightbox__pips {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 24px;
  display: flex;
  justify-content: center;
  gap: 7px;
}
.lightbox__pip {
  width: 7px;
  height: 7px;
  border-radius: var(--liquid-radius-capsule);
  background: rgba(255, 255, 255, 0.35);
}
.lightbox__pip--on { background: #FFFFFF; }

/* ══ PING CARDS ═══════════════════════════════════════════════════════════
 *
 * The app's PingCardShell, in CSS. See partial-ping-card.html for what each
 * part is and lib-ping-cards.go for what each part is allowed to say.
 *
 * A card rather than the .row that was here: type, timing and the channel post's
 * pictures are the three things a reader scans a ping list for, and a row
 * carried none of them — a paused place ping and a live plain ping were the
 * same grey line, and the paused one looked like it was working.
 *
 * WHY THE ACCENT IS ON THE GLYPH AND THE SONAR AND NOWHERE ELSE. The card is
 * CONTENT: a neutral surface with the ordinary content hairline, exactly like a
 * channel post card. The type's colour lives in the tinted squircle and the status ring,
 * which is what replaced the accent-tinted border the first version of this had.
 * A list of six cards in six colours is a list nobody can scan. */

/* THE POSITIONED BOX. A <details> inside an <a> is invalid HTML the browser
 * repairs by closing the anchor early, which leaves a card where half the
 * surface silently is not the link it looks like — so the menu is a SIBLING of
 * the anchor and this is what it is placed against. partial-channel-cards.html and
 * the ping rows this replaced both solved the identical problem the same way.
 *
 * Its position: relative is set once, up with .cardwrap, beside the
 * rules that place the button against it — restating it here would be a second
 * place for the three wrappers to disagree. */
.acardwrap { margin: 0 0 16px; }

.acard {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card);
  color: inherit;
  text-decoration: none;
  transition: transform 0.12s ease;
}
/* NO HOVER TINT (2026-09-24). The card answers a press the way an Inbox card
 * does — a small give on :active — and does not change colour under a pointer
 * that is only passing over it. */
a.acard:active { transform: scale(0.975); }

/* A MUTED PING IS STILL RECEIVED — it is only not being delivered. The fade
 * says that without a second card state, and the menu says it in words. */
/* STEPPED BACK, NOT FADED — PingCardShell, 2026-09-21. A paused or muted card
 * keeps its white surface and dims its text and glyph by liquidReadDim (0.62);
 * only a COMPLETED card fades as a whole, at kDimmedOpacity 0.55. */
.acard--muted .acard__glyph,
.acard--muted .acard__header,
.acard--muted .acard__message,
.acard--muted .gallery { opacity: 0.62; }
.acard--done { opacity: 0.55; }

/* Room for the ⋯, which floats over the card's top-right corner. Without this
 * a long header line runs underneath it. */
.acard__head {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* THE TYPE GLYPH, in the accent-tinted squircle — the iOS list idiom that
 * replaced a text pill. 38px is the app's own size.
 *
 * A SQUIRCLE IS NOT A ROUNDED RECTANGLE, and CSS has no superellipse: this is
 * the closest the platform offers, at the radius the app takes at face value
 * since the 2.35 fudge was retired. */
.acard__glyph {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 12px;
  color: var(--acard-accent);
  background: var(--acard-tint);
}
.acard__glyph .nav__glyph { width: 20px; height: 20px; }

/* ONE LINE, TRUNCATED. The header carries the timing or the sender and both are
 * short; what must never happen is a two-line header pushing the status
 * indicator off the row it belongs to. */
.acard__header {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--type-small);
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── THE STATUS INDICATOR ───────────────────────────────────────────────────
 *
 * Three values and they are not interchangeable: a check means it was sent, a
 * pause means the owner stopped it, and ACTIVE draws no glyph at all — it is the
 * ring below, because "more of these are coming" is a thing in motion and a
 * static mark for it would have to be read rather than noticed.
 *
 * The element is role="img" with the state's ARB string as its label, so what a
 * screen reader gets is the word and not the shape. */
.acard__status {
  flex: 0 0 auto;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin: -7px 0;
  color: var(--acard-accent);
}
.acard__status .nav__glyph { width: 16px; height: 16px; }

/* The dot, and the ring expanding out of it. Two pseudo-elements on one span:
 * the dot is the state and the ring is the motion, and only the live state has
 * both. */
.acard__status--live::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--acard-accent);
}
.acard__status--live::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--liquid-radius-capsule);
  border: 1.5px solid var(--acard-accent);
  animation: acard-sonar 2.2s ease-out infinite;
}
@keyframes acard-sonar {
  0%   { transform: scale(0.3); opacity: 0.5; }
  100% { transform: scale(1);   opacity: 0;   }
}

/* A PAUSED PING'S MARK IS GREY, NOT THE TYPE ACCENT. The accent means "this is
 * a plain ping"; on the one card where nothing is happening it would read as
 * emphasis on a thing that has stopped. The app makes the same swap. */
.acard__status--paused { color: var(--liquid-secondary-label); }

.acard__message {
  font-size: var(--type-body);
  font-weight: 400;
  line-height: 1.35;
  color: var(--liquid-primary-label);
  /* Two lines and then an ellipsis, which is the app's own cap. A card is the
   * reason to open the ping, not the ping. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* THE PICTURES, AT CARD SCALE. The same strip the reading surfaces draw, one
 * token shorter: a card is one row of a scrolling list and its whole body is two
 * lines of text, so a strip taller than this stops being the channel post's pictures and
 * becomes the channel post. 96 against the reading surface's 220 is roughly the ratio
 * the app's 76 against its own 220 strikes, on a column twice as wide.
 *
 * NOTHING IN IT ANSWERS A TAP — the tiles are plain images, so a press falls
 * through to the anchor and opens the ping, which is what a press anywhere else
 * on the card does. See partial-photo-strip.html. */
.gallery--card { --gallery-h: 76px; margin: 0; }

/* ── RETIRED — .pcard .gallery ───────────────────────────────────────────────
 *
 *   .pcard .gallery { --gallery-h: 150px; }
 *
 * A channel post card's strip, between the two, on the argument that the
 * column is 608px and the card is the reading surface — a channel is read from
 * its timeline.
 *
 * IT HAS NO ELEMENT LEFT TO MATCH since 2026-09-20. A card draws a capped body
 * and ONE thumbnail now, which is the shape the app has always had; the gallery
 * lives on the update's own page. See channelPostCard.CardMedia for the whole
 * argument, and .pcard__thumbimg for what replaced this.
 *
 * Kept as a note rather than deleted because the premise is the part worth
 * remembering: if a strip ever comes back to this card, 150 was measured
 * against this column and is where to start. */

/* ── THE TYPE ACCENTS ───────────────────────────────────────────────────────
 *
 * One pair per type, and they are the app's own constants: kPlainPrimary,
 * kPlacePrimary and kSocialPrimary from lib/models. Declared as a variable on
 * the card rather than as a colour on each part, so the glyph, its tint and the
 * sonar cannot disagree about which type they belong to.
 *
 * THESE ARE NOT THE DOMAIN ACCENT. --liquid-accent is "this is a ping", which
 * is what every ping AUTHORING surface paints; these are "this is a place ping
 * rather than a channel post one", which is what a mixed list needs and an authoring
 * screen does not. Both exist in the app for exactly that reason.
 *
 * PLACE AND SOCIAL HAVE ONE VALUE EACH AND NOT A PAIR, which is not an
 * omission here: kPlacePrimary and kSocialPrimary are single consts in the
 * app too, used unchanged in both themes. Only the channel post blue has a declared dark
 * half. What does change for all three is the TINT, because a 14% wash over
 * #1E293B is very nearly invisible. */
/* ONE PING BLUE SINCE THE APP'S PALETTE PASS: kPlainPrimary is
 * Palette.pingLight now, and place and social point at the same pair
 * (ping_type_spec.dart) — the per-type blue, cyan and red above are retired
 * there and were left behind here until 2026-09-21. The classes stay per type
 * so a future split is a one-line change. */
.acard--plain,  .nrow--plain,
.acard--place,  .nrow--place,
.acard--social, .nrow--social { --acard-accent: #2D4F9E; --acard-tint: rgba(45, 79, 158, 0.14); }

/* ── AND THE FOUR THE INBOX ADDS ────────────────────────────────────────────
 *
 * A ping card only ever draws three types. The inbox draws everything that
 * happens to an account, so it needs four more — and the pairs are the app's
 * own, from NotificationGlyph.accentFor.
 *
 * CHANNEL goes through --liquid-channel-accent rather than repeating the
 * orange: one domain accent per act, declared once, and the tint is the only
 * thing stated here because a colour-mix over a token is how the wash follows
 * the theme without a second hex to forget.
 *
 * CREDIT is amber and SECURITY is red, and neither is the ping blue for a
 * reason that matters on a list somebody scans: "you are out of credits" and
 * "somebody signed in on a new device" are the two rows in this list a reader
 * must not scroll past, and they are the two the domain colours would have made
 * look like every other ping.
 *
 * ACCOUNT IS NEUTRAL ON PURPOSE. A request, an approval, a plan change, a
 * pause: ordinary administration, and painting it in an accent would put six
 * colours on one screenful and leave nothing for the two rows above to stand
 * out from. */
.nrow--channel  { --acard-accent: var(--liquid-channel-accent); --acard-tint: rgba(191, 92, 8, 0.14); }
.nrow--credit   { --acard-accent: #8F6500; --acard-tint: rgba(143, 101, 0, 0.14); }
.nrow--security { --acard-accent: #A8182E; --acard-tint: rgba(168, 24, 46, 0.14); }
.nrow--account  { --acard-accent: #7A7066; --acard-tint: rgba(122, 112, 102, 0.14); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .acard--plain,  :root:not([data-theme="light"]) .nrow--plain,
  :root:not([data-theme="light"]) .acard--place,  :root:not([data-theme="light"]) .nrow--place,
  :root:not([data-theme="light"]) .acard--social, :root:not([data-theme="light"]) .nrow--social { --acard-accent: #7C9CFF; --acard-tint: rgba(124, 156, 255, 0.22); }
  :root:not([data-theme="light"]) .nrow--channel  { --acard-tint: rgba(255, 162, 79, 0.22); }
  :root:not([data-theme="light"]) .nrow--credit   { --acard-accent: #F4C542; --acard-tint: rgba(244, 197, 66, 0.22); }
  :root:not([data-theme="light"]) .nrow--security { --acard-accent: #FF6B7A; --acard-tint: rgba(255, 107, 122, 0.22); }
  :root:not([data-theme="light"]) .nrow--account  { --acard-accent: #A39A8F; --acard-tint: rgba(163, 154, 143, 0.22); }
}
:root[data-theme="dark"] .acard--plain,  :root[data-theme="dark"] .nrow--plain,
:root[data-theme="dark"] .acard--place,  :root[data-theme="dark"] .nrow--place,
:root[data-theme="dark"] .acard--social, :root[data-theme="dark"] .nrow--social { --acard-accent: #7C9CFF; --acard-tint: rgba(124, 156, 255, 0.22); }
:root[data-theme="dark"] .nrow--channel  { --acard-tint: rgba(255, 162, 79, 0.22); }
:root[data-theme="dark"] .nrow--credit   { --acard-accent: #F4C542; --acard-tint: rgba(244, 197, 66, 0.22); }
:root[data-theme="dark"] .nrow--security { --acard-accent: #FF6B7A; --acard-tint: rgba(255, 107, 122, 0.22); }
:root[data-theme="dark"] .nrow--account  { --acard-accent: #A39A8F; --acard-tint: rgba(163, 154, 143, 0.22); }

/* ══ THE PING DETAIL PAGE ═════════════════════════════════════════════════
 *
 * The app's four cards — MESSAGE, SCHEDULE, WHO GETS IT, HISTORY — in the
 * grouped-panel shape this site already uses for standing state. See
 * page-ping-detail.html for what each says and lib-ping-detail.go for what
 * each is allowed to say.
 *
 * A PANEL RATHER THAN A CARD, which is the opposite of the pings LIST and is
 * deliberate: a list is objects you choose between, a detail page is one
 * object's properties, and the site's own rule is that verbs go on a capsule,
 * links go in the rail, and standing state goes in a grouped list. */

/* PROSE INSIDE A PANEL NEEDS THE PADDING A ROW BRINGS ITS OWN OF. .group has
 * none — every child so far has been a .row, which pads itself — and a block
 * renderer dropped straight in would sit flush against the panel's edge. */
.group__body { padding: 14px; }

/* The renderer's last block carries a bottom margin meant to separate it from
 * the next one. Inside a panel there is no next one, and the margin reads as a
 * panel that did not close properly. */
.group__body .blocks > :last-child { margin-bottom: 0; }

/* A row's trailing VALUE, where .row__subtitle is a row's second LINE. Two
 * different shapes: "Ends — 4 October 2026" is one line with the fact on the
 * right, and a subtitle under the label would make a four-row schedule eight
 * rows tall. */
.row__value {
  flex: 0 0 auto;
  font-size: var(--type-heading);
  color: var(--liquid-secondary-label);
  text-align: right;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ── A NOTE AT THE FOOT OF A PANEL ──────────────────────────────────────────
 *
 * One sentence explaining the panel above it, inside the panel rather than
 * under it — the place ping's "use the device where this ping was created".
 *
 * IT IS INSIDE BECAUSE IT IS ABOUT THIS PANEL. A sentence floating between two
 * groups belongs to neither and reads as page copy; the whole reason it exists
 * is that a column of values with no edit control reads as fields somebody
 * forgot to finish, and an explanation that is not visibly attached does not
 * answer that.
 *
 * The padding matches .row's own so the text lines up with the labels above it
 * rather than sitting in its own indent. */
.group__note {
  margin: 0;
  padding: 4px 14px 14px;
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
}

/* ── THE LOCK ───────────────────────────────────────────────────────────────
 *
 * The mark beside a value a plan does not cover. SHOWN WITH A LOCK RATHER THAN
 * HIDDEN is the standing rule: a capability nobody can see is one nobody knows
 * they are missing, and the value beside it stays legible — the lock qualifies
 * it, it does not replace it.
 *
 * QUIET, AND DELIBERATELY NOT THE ACCENT. It is not a control and must not read
 * as one; an accent-coloured padlock in a row of grey values is the loudest
 * thing on the page, which would make every locked row look like an error. The
 * secondary label with opacity on top rather than a third label colour — see
 * the .cardmenu__btn note for why this codebase has no --liquid-tertiary-label.
 *
 * Inline-flex with a baseline-ish nudge because it sits INSIDE .row__value
 * after the words, and a bare inline <svg> would sit on the text baseline and
 * hang below it. */
.lockmark {
  display: inline-flex;
  align-items: center;
  margin-left: 6px;
  vertical-align: -2px;
  color: var(--liquid-secondary-label);
  opacity: 0.75;
}
.lockmark .nav__glyph { width: 15px; height: 15px; }

/* ── THE TIME, AND THE RULE THAT REPEATS IT ─────────────────────────────────
 *
 * THE CLOCK LEADS AND IT IS LARGE. On a screen about when something happens the
 * time is the fact and the recurrence is the qualifier, which is the hierarchy
 * _BigTime draws in the app. A 15px "09:30" in a row labelled "Time" is the same
 * information arranged so that nothing about it is legible at a glance.
 *
 * Tabular figures so the digits do not shuffle between one ping and the next —
 * "11:00" and "09:30" are the same width, which is what stops a list of these
 * looking hand-set. */
.sched {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 16px 14px 14px;
  flex-wrap: wrap;
}
.sched__time {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--liquid-primary-label);
}

/* The recurrence, as a capsule beside the clock — the app's frequency badge. In
 * the ping domain's accent, because "Daily" is a fact about the ping rather
 * than about its type: a place ping and a plain ping that both repeat daily say
 * the same word, and colouring it per type would suggest otherwise. */
.sched__freq {
  display: inline-block;
  padding: 5px 12px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-fill-quiet);
  color: var(--liquid-accent);
  font-size: var(--type-small);
  font-weight: 600;
}

/* ── THE DAY BOXES ──────────────────────────────────────────────────────────
 *
 * The app's own weekday picker, minus the ability to press one. All seven are
 * drawn rather than only the chosen ones, because "Mon, Thu" and "every day
 * except Tuesday" are read off the same row and the second is only legible when
 * the gaps are visible.
 *
 * The unselected days are not hidden and not faded to nothing: they are the
 * reference the selection is read against. */
.daybar { display: flex; gap: 5px; flex-wrap: wrap; }
.daybar__day {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-fill-quiet);
  color: var(--liquid-secondary-label);
  font-size: var(--type-caption);
  font-weight: 600;
}
/* FILLED, NOT OUTLINED. A ring around a 30px box at this size is two pixels of
 * paint and disappears at 1x; a filled capsule is legible at any density and
 * without colour vision, because the contrast is the signal. */
.daybar__day--on {
  background: var(--liquid-accent);
  color: #FFFFFF;
}

/* ══ THE FROM SECTION ══════════════════════════════════════════════════════
 *
 * Who published this, at the FOOT of a channel post, under a heading of its own. See
 * partial-ping-from.html for why it is at the foot and not the top — the short
 * version is that a breadcrumb puts the publication in front of the thing the
 * reader opened.
 *
 * IT IS A .group AND A .row AT ONCE, which is not a shortcut: it is a
 * single-row grouped panel, and the alternative — a .group wrapping one .row —
 * is a wrapper element whose only job is to be a wrapper. The row's own padding
 * and the panel's own fill compose exactly the way they do in a list of several.
 *
 * A LINK, unlike the app's sender row, and that is the same rule rather than a
 * different one: the app draws no chevron and answers no tap on a PERSON,
 * because there is no per-user profile route. A channel has a screen. */
.pingfrom { display: flex; }

/* Larger than the 36px a card header carries. This is the section's subject
 * rather than a card's attribution line, and the app's own FROM row is a 64pt
 * object for the same reason. */
.pingfrom__avatar { width: 44px; height: 44px; border-radius: 12px; font-size: 19px; }

/* ══ THE INBOX ROW ═════════════════════════════════════════════════════════
 *
 * A glyph square, a header, a body, the pictures and a foot — the app's shape
 * since 2026-09-15. What was here before was .row: a composed headline, a grey
 * subtitle and a button, with no mark, no media and no read state at all
 * (.row--unread had a class in the markup and not one rule in this file).
 *
 * # NOT A CARD, AND NOT THE PING CARD
 *
 * This list is the one surface in the product that grows without limit — every
 * ping, every account notice, every request — so a row's job is to be SCANNED
 * PAST. It keeps the panel's hairline separators rather than becoming thirty
 * floating cards, and it borrows the ping card's glyph square because that
 * square is the site's idiom for "what kind of thing is this", not because the
 * two are the same object. */
.nrow {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px 10px 16px;
  position: relative;
}

/* The hairline between rows, matching .row + .row — a list of rows is a panel,
 * and a panel's rows are divided rather than spaced. */
.nrow + .nrow::before {
  content: "";
  position: absolute;
  top: 0;
  left: 16px;
  right: 0;
  height: 1px;
  background: var(--liquid-content-border);
}

/* THE SQUARE IS THE PING CARD'S, two points smaller. 38px beside a two-line
 * card is right; beside a row whose text may be one line it is the tallest
 * thing on the row by half. The radius follows it down so the corner keeps the
 * same curvature ratio. */
.nrow__glyph {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 11px;
  color: var(--acard-accent);
  background: var(--acard-tint);
}
.nrow__glyph .nav__glyph { width: 19px; height: 19px; }

/* THE TEXT COLUMN IS THE LINK, for the reason page-notifications.html gives at
 * length: a <details> and a <form> cannot live inside an <a>. min-width: 0 is
 * what lets the two text lines ellipsise inside a flex child. */
/* RETIRED 2026-09-22: .nrow__text, the row's link column. The Inbox row is a
 * card now and the whole card is the link; see .ncard. */

/* ONE LINE, TRUNCATED, and it is the line the reader scans. */
.nrow__header {
  font-size: var(--type-small);
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* TWO LINES AND THEN AN ELLIPSIS. The app caps it at two for the same reason:
 * a notice that runs to a paragraph would otherwise make one row as tall as
 * four, on the one list nobody reads top to bottom. The whole message is on the
 * surface the row opens. */
.nrow__body {
  font-size: var(--type-small);
  color: var(--liquid-secondary-label);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The strip is shorter here than on a card. A card is one object and its
 * pictures are most of it; a row is one of thirty and its pictures are a hint
 * that there are some. */
.nrow .gallery--card { --gallery-h: 72px; margin-top: 2px; }

/* ── RETIRED 2026-10-07 — .nrow__voice, .nrow__voiceglyph, .nrow__link,
 * .nrow__linktext: the voice chip and the link chip, each on a line of its own
 * under the photo strip. Every attachment is a tile of one strip now — see
 * .astrip. They were:
 *
 *   .nrow__voice { align-self: flex-start; color: var(--acard-accent); gap: 5px; }
 *   .nrow__voiceglyph { display: inline-flex; width: 13px; height: 13px; }
 *   .nrow__voiceglyph .nav__glyph { width: 13px; height: 13px; }
 *   .nrow__link { max-width: 100%; min-width: 0; }
 *   .nrow__linktext { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
 */

/* THE FOOT, BOTTOM RIGHT — "From @ana", the line a received ping card ends in.
 * Right-aligned because that is where the card puts it and because the two
 * lines above are left-aligned: a third left-aligned line would read as a
 * continuation of the message rather than as attribution. */
.nrow__foot {
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--liquid-secondary-label);
}
.nrow__footglyph { display: inline-flex; width: 13px; height: 13px; }
.nrow__footglyph .nav__glyph { width: 13px; height: 13px; }

/* The pill and the mark-read button, stacked at the row's right edge and
 * OUTSIDE the anchor. */
/* RETIRED 2026-09-22: .nrow__side, the pill and mark-read button at the row's
 * right. The pill rides the card's edge now and mark-read is in the ⋮ menu. */

/* ── READ IS THE MARKED STATE ───────────────────────────────────────────────
 *
 * The same way round as the channel timeline's .pcard--read, and for the same
 * reason: a list is mostly things you have already seen, so this is one quiet
 * treatment applied to the majority rather than a loud one applied to whatever
 * is new today. Unread is simply the row at full strength.
 *
 * OPACITY ON THE TEXT AND NOT ON THE ROW. Fading the whole element takes the
 * glyph tint and the reaction pill with it, and a row at 70% reads as
 * half-loaded rather than as read. The app steps back the LABEL colours, which
 * is what this does.
 *
 * THE INBOX MARKS EVERY ROW WHERE THE TIMELINE DRAWS ONE DIVIDER. That is the
 * difference between the two lists: a timeline is scrolled past in publication
 * order, so a boundary answers it, while this list is triaged row by row and
 * each row is a different kind of thing. There is no single line to draw across
 * it. */
.nrow--read .nrow__header { color: var(--liquid-secondary-label); font-weight: 500; }
.nrow--read .nrow__body,
.nrow--read .nrow__foot { opacity: 0.75; }
.nrow--read .nrow__glyph { opacity: 0.7; }

/* ── THE "+2" ON THE LAST TILE ──────────────────────────────────────────────
 *
 * Only ever drawn where a strip is built from a content HINT, which carries at
 * most six thumbnails and a count that may be larger — see
 * partial-photo-strip.html.
 *
 * A SCRIM ACROSS THE WHOLE TILE rather than a badge in its corner. The badge
 * was the first shape and it disappeared over a light photograph; the wash also
 * says the right thing, which is that this tile is standing in for the rest
 * rather than being one more picture with a number stuck on it. */
/* position: relative lives on .gallery__tile itself (above), because a scrim
 * positioned against the STRIP rather than the tile would land at the edge of a
 * row the reader can drag sideways. */
.gallery__more {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: inherit;
  background: rgba(0, 0, 0, 0.45);
  color: #FFFFFF;
  font-size: 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ══ SOMEBODY ELSE'S PROFILE ════════════════════════════════════════════════
 *
 * A centred identity block and two figures under it, over the Discover tiles
 * for whatever they publish. The app's person screen in the same order, and
 * the tiles below are literally the catalogue's — see partial-channel-cards.
 *
 * CENTRED, which nothing else on this site is. It is the one page whose whole
 * subject is a single person: a left-aligned name over a left-aligned handle
 * reads as the first row of a list that never arrives. */
.person {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 24px 16px 20px;
}

/* 72px, between the ping FROM row's 44 and nothing larger: big enough to be
 * the subject of the page, small enough that the name is still above the fold
 * on a phone. The placeholder and the photograph are the same box so the page
 * does not reflow when the image lands. */
.person__avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 72px;
  height: 72px;
  border-radius: 24px;
  object-fit: cover;
  color: #FFFFFF;
  font-size: 30px;
  font-weight: 600;
  overflow: hidden;
}

/* The eight tints, and they are the SAME eight the avatar placeholder uses
 * everywhere else — .pcard__avatar--tN above. Restated rather than shared by
 * adding a second class in the markup, because a template that has to remember
 * two classes to draw one square is a template that will one day carry one. */
.person__avatar--t0 { background: #B85400; }
.person__avatar--t1 { background: #4F46E5; }
.person__avatar--t2 { background: #C026D3; }
.person__avatar--t3 { background: #E11D48; }
.person__avatar--t4 { background: #D97706; }
.person__avatar--t5 { background: #0284C7; }

/* ── RETIRED 2026-09-24 — .person__name, .person__handle, .person__stats,
 *    .person__stat, .person__statn, .person__statlabel ─────────────────────
 *
 * The person page's own header: the name and @handle under the avatar, and
 * two figures side by side with a hairline between them. page-person.html
 * took the profile hero (.phero) on 09-23, the one header Profile and a
 * person share, so these drew nothing. The avatar tints above stay: .phero
 * still wears person__avatar--tN. */

/* ══ THE CIRCLES STRIP, ON THE PEOPLE PAGE ══════════════════════════════════
 *
 * The same control as .channelstrip and deliberately so — see
 * templates/partial-circle-strip.html. What differs is the emblem, and only the
 * emblem: it is ROUND. A channel wears a rounded square because a circle reads
 * as a person and a channel is a publication; a circle IS a group of people, so
 * the shape that is wrong there is right here.
 *
 * NOT MERGED WITH .channelstrip INTO ONE CLASS. They share five declarations
 * and differ in the one that carries the meaning, and a shared base would put
 * the shape rule in an override that the next person to touch either file can
 * flatten without noticing what it was for. Two controls that look alike is how
 * one of them quietly loses its scroll snapping; two controls that ARE alike in
 * every way that matters and differ in the one that does not is a merge worth
 * refusing. */
/* RETIRED 2026-09-28 — `.group--strip { padding-left: 0; padding-right: 0; }`:
 * the strip sat inside a white card. The app's _CirclesStrip sits on the page,
 * as the channel strip does, so the row is bare now; see .circlestrip. */
/* RETIRED 2026-09-21 — `.group--strip .group__headrow { padding: 0 14px; }`: the
 * header row moved above the panel, where every section header sits. */

.circlestrip {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  /* ON THE PAGE, NOT IN A CARD (2026-09-28), as .channelstrip is: room round
   * the tiles for the focus ring, given back by the negative margin so the
   * first emblem lines up with the header above it. */
  padding: 4px 10px 8px;
  margin: -4px -10px 16px;
  scroll-snap-type: x proximity;
  scroll-padding-left: 10px;
  -webkit-overflow-scrolling: touch;
}

/* A POINTER GETS A BAR, for the reason .channelstrip gives: a wheel does not
 * scroll this and a hidden bar leaves the far end of the row unreachable. Both
 * syntaxes, because one of them is Firefox and the other is everybody else. */
/* The pointer-only bar moved to [data-hscroll], above, on 2026-09-22. */

.circlestrip__tile {
  flex: 0 0 auto;
  width: 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  scroll-snap-align: start;
  color: inherit;
  text-decoration: none;
}
.circlestrip__tile:focus-visible {
  outline: 2px solid var(--liquid-accent);
  outline-offset: 3px;
  border-radius: 12px;
}

.circlestrip__emblem {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  /* ROUND. The one line in this block that is not the channel strip's. */
  border-radius: 50%;
  object-fit: cover;
  overflow: hidden;
  color: #fff;
}
.circlestrip__emblem svg { width: 30px; height: 30px; }

/* ── RETIRED 2026-09-24 — .circlestrip__emblem--c0 … --c7, --default ───────
 *
 * The flat disc's background: the eight colours the circle editor offers
 * (circleTintIndex's positions), and the domain pink for an uncoloured
 * circle. The strip's tile is the circle emblem now (.circlestrip__emblem--ring,
 * partial-circle-emblem.html), which paints the circle's own colour into its
 * band, so no tile carries a --cN or --default class. */

/* THE CREATE TILE IS DASHED AND NOT FILLED. It is not one of the reader's
 * circles and must not be mistaken for one at a glance, which is exactly what a
 * ninth coloured disc would be. */
.circlestrip__emblem--new {
  background: transparent;
  border: 1.5px dashed var(--liquid-separator);
  color: var(--liquid-secondary-label);
  box-sizing: border-box;
}
.circlestrip__tile:hover .circlestrip__emblem--new {
  border-color: var(--liquid-accent);
  color: var(--liquid-accent);
}

.circlestrip__name {
  max-width: 84px;
  font-size: 12px;
  line-height: 1.25;
  text-align: center;
  color: var(--liquid-secondary-label);
  /* ONE LINE, ellipsised — the channel strip's argument exactly: a circle
   * called "Sunday football, the whole lot of them" would be four lines tall
   * and push every tile beside it down. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ══ THE EMPTY STATE THAT IS A SCREEN ═══════════════════════════════════════
 *
 * .empty is one grey paragraph inside a list that happens to have nothing in
 * it. This is the other kind: the page has no content at all, the reader needs
 * to be told why in more than a clause, and there is somewhere to send them.
 * The app's EmptyStateView, which is title, subtitle and one action.
 *
 * FIRST USED BY the place-request picker with nobody in it, where the old shape
 * — a grey line and then the form anyway — let somebody fill in six fields
 * before being refused on the one they could not answer. */
.emptystate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 32px 18px;
  text-align: center;
}
.emptystate__title {
  margin: 0;
  font-size: var(--type-heading);
  font-weight: 700;
  color: var(--liquid-primary-label);
}
.emptystate__body {
  margin: 0;
  /* Capped, because a centred paragraph running the width of a desktop panel is
   * a paragraph nobody finishes — the same 520px the search field caps at. */
  max-width: 520px;
  color: var(--liquid-secondary-label);
}
/* The button gets its own air: it is the way out, and a filled pill sitting
 * directly under the last line of a paragraph reads as part of it. */
.emptystate .btn { margin-top: 6px; }

/* ── THE PLAN LOCK ──────────────────────────────────────────────────────────
 *
 * The sentence under a control a plan does not cover. See
 * templates/partial-plan-lock.html for the rule it serves and lib-plan-gate.go
 * for why it never disables anything.
 *
 * SECONDARY LABEL, NOT A PING COLOUR. This is a fact about the plan, not a
 * problem with what the reader did — the same weight liquidLockedLabel gives a
 * locked chip in the app. Drawing it in the destructive red would read as a
 * validation failure on a form nobody has submitted yet.
 *
 * NO PANEL AND NO FILL. It sits inside a .group, directly under the control it
 * is about, and a tinted box around it would make it a second card in a list of
 * cards and break the association with the row above. Indented to the row's
 * text column for the same reason: it belongs to that row.
 *
 * Wraps rather than truncating, because these sentences are two lines on a
 * phone and the second line is usually the half that names the way forward. */
.locknote {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin: -4px 0 4px;
  padding: 0 14px 12px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--liquid-secondary-label);
}
.locknote__text { flex: 1 1 14ch; }

/* The way forward, in the link blue rather than the button emerald: this is a
 * link to a page, not the commit on this form, and the only filled button in
 * any group must stay the one that submits it. */
.locknote__action {
  flex: 0 0 auto;
  font-weight: 600;
  white-space: nowrap;
}

/* ══ THE CREATE LIST — /pings/new (2026-09-21) ═══════════════════════════════
 *
 * create_intents_sheet.dart's rows: a 40pt chip at 12% of the accent behind a
 * 20pt glyph, 14 to the text, the label 17/600, the example 13 at 55% of the
 * label colour, 12 above and below. The dividers start under the text (58 in
 * the app: 4 + 40 + 14), which is 16 + 40 + 14 here with the card's own inset.
 * A row the web cannot make is not a link and carries one more line. */
.intents { padding: 4px 0; }
.intent {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  color: inherit;
  text-decoration: none;
}
a.intent:hover { background: var(--liquid-fill-quiet); }
.intent + .intent::before {
  content: "";
  position: absolute;
  top: 0;
  left: 70px;
  right: 0;
  height: 1px;
  background: var(--liquid-separator);
}
.intent__chip {
  --tint: var(--liquid-ping-accent);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  color: var(--tint);
  background: color-mix(in srgb, var(--tint) 12%, transparent);
}
.intent__chip--channel { --tint: var(--liquid-channel-accent); }
.intent__chip .nav__glyph { width: 20px; height: 20px; }
.intent__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.intent__label { font-size: 17px; font-weight: 600; color: var(--liquid-primary-label); }
.intent__example {
  font-size: 13px;
  color: var(--liquid-primary-label);
  opacity: 0.55;
}
/* The app's create-sheet pill: Unlimited / "3 of 10 used" / full. One neutral
   colour for every count; error red only when the row will not open a form. */
.intent__quota {
  align-self: flex-start;
  margin-top: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.2px;
  color: var(--liquid-secondary-label);
  background: var(--liquid-fill-quiet);
}
.intent__quota--full {
  color: var(--liquid-destructive);
  background: color-mix(in srgb, var(--liquid-destructive) 12%, transparent);
}
.intent__note {
  margin-top: 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--liquid-secondary-label);
}

.actions--center { justify-content: center; }

.intentverbs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: 18px;
  row-gap: 2px;
  margin: 0 0 var(--space-section);
}
.intentverbs a {
  padding: 8px 4px;
  font-size: 15px;
  font-weight: 600;
  color: var(--liquid-accent);
  text-decoration: none;
}
.intentverbs a:hover { text-decoration: underline; }

/* ── THE SAME LIST IN THE RAIL'S CREATE FLYOUT (2026-09-22) ──────────────────
 *
 * The app's ＋ card: 20 12 16 padding, a centred 17/600 title, rows at 4 from
 * the card's gutter (hairline at 58 = 4 + 40 + 14), the verbs pinned at the
 * foot. Wider than the other flyouts because every row carries two lines, and
 * the list scrolls rather than the card growing past the window. */
.nav__createmenu {
  width: 380px;
  max-width: calc(100vw - 120px);
  max-height: calc(100vh - 32px);
  padding: 20px 12px 16px;
  border-radius: var(--liquid-radius-overlay);
}
.nav__createtitle {
  margin: 0 0 10px;
  text-align: center;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--liquid-primary-label);
}
.nav__createmenu .intents { padding: 0; overflow-y: auto; min-height: 0; }
.nav__createmenu .intent { padding: 12px 4px; border-radius: var(--liquid-radius-row); }
.nav__createmenu .intent + .intent::before { left: 58px; height: 0.5px; }
.nav__createmenu .intentverbs { margin: 14px 0 0; flex: 0 0 auto; }
@media (max-width: 700px) {
  .nav__createmenu { width: auto; max-width: none; max-height: calc(100vh - 120px); }
}

/* ══ THE INBOX, AT THE APP'S SHAPE — 2026-09-22 ═══════════════════════════
 *
 * inbox_screen.dart: a search row with the list's ⋮ menu at its end, the
 * sender strip, day chips, and squircle cards in up to three columns. The
 * numbers are the app's: card padding 16/10/12/10, a 28px glyph squircle
 * (radius 9) with a 16px mark, header 15/600 with the time 13px in the
 * tertiary ink, message 15/1.4 over two lines, foot 13px, a 12px gap between
 * cards (LiquidSpace.stack) and 20 between columns. A read card steps back by
 * the app's liquidReadDim factor, 0.62. */

/* The search row: the field takes the width, the ⋮ sits at its end. */
.inboxbar { display: flex; align-items: center; gap: 4px; margin: 0 0 12px; }
.inboxbar > .group--search { flex: 1 1 auto; margin: 0; min-width: 0; }
.inboxbar__menu > .cardmenu__btn { width: 40px; height: 40px; background: transparent; }
.inboxbar__menu > .cardmenu__btn:hover,
.inboxbar__menu[open] > .cardmenu__btn { background: var(--liquid-fill-quiet); }
/* An empty inbox draws the row at 40% and takes no input, as the app does. */
.inboxbar--inert { opacity: 0.4; pointer-events: none; }

/* THE SENDER STRIP: the channel strip, round. */
.sendstrip .channelstrip__disc,
.sendstrip .channelstrip__mark,
.sendstrip .channelstrip__img { border-radius: 50%; }
/* All: Palette.ping with the white bell (2026-09-28, was Palette.info). */
.sendstrip__disc--all { background: #2D4F9E; color: #fff; }
/* Aletto: Palette.brandMid under the logo, the brand mark if it fails. */
.sendstrip__disc--aletto { background: #0C8863; color: #fff; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sendstrip__disc--all { background: #7C9CFF; color: #1F1B16; }
}
:root[data-theme="dark"] .sendstrip__disc--all { background: #7C9CFF; color: #1F1B16; }
/* The ring is emerald on every tile (_SenderStripTile), not the tile's tint. */
.sendstrip .channelstrip__tile--selected .channelstrip__disc {
  box-shadow:
    0 3px 12px rgba(12, 136, 99, 0.32),
    0 0 0 4px var(--liquid-grouped-background),
    0 0 0 6.5px #0C8863;
}
/* The badge is Palette.urgent. */
.sendstrip__badge { background: #A8182E; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sendstrip__badge { background: #FF6B7A; color: #1A0508; }
}
:root[data-theme="dark"] .sendstrip__badge { background: #FF6B7A; color: #1A0508; }

/* A PERSON TILE'S MENU. The cell holds the tile and its ⋮, which sits on the
 * disc's top-right corner, smaller than a card's. The panel is position:
 * fixed and placed under the tile by layout.html, because the strip scrolls
 * sideways and a scroller clips whatever is positioned inside it. */
.sendstrip__cell { position: relative; flex: 0 0 auto; display: flex; scroll-snap-align: start; }
.sendstrip__cell > .cardmenu { position: absolute; top: -6px; right: -2px; z-index: 2; }
.sendstrip__cell > .cardmenu > .cardmenu__btn { width: 24px; height: 24px; background: var(--liquid-content-surface); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22); }
.sendstrip__cell .cardmenu__glyph { width: 15px; height: 15px; }
.cardmenu--float > .cardmenu__panel { position: fixed; right: auto; top: 0; left: 0; z-index: 60; }

/* THE DAY CHIP: a small pill over the cards (_DayHeaderDelegate), 13/500 with
 * 0.4 tracking in the secondary ink, on the flat chrome fill. Sticky, as the
 * app pins it. */
.daychip {
  position: sticky;
  top: 8px;
  z-index: 5;
  margin: 12px 0 12px 4px;
  font: inherit;
}
.daychip:first-of-type { margin-top: 4px; }
.daychip > span {
  display: inline-block;
  padding: 5px 10px;
  border-radius: 12px;
  background: var(--liquid-flat-chrome-fill);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.4px;
  line-height: 1.35;
  color: var(--liquid-secondary-label);
}

/* ONE COLUMN. The app goes to two or three on a tablet, but the web draws
 * every list in its single reading column (about 600px), and two cards side by
 * side in it were 290px each: cramped, and unlike any other list on the site. */
.ncards { display: block; }

.ncardwrap {
  position: relative;
  break-inside: avoid;
  /* Room below for the reaction pill that rides the card's bottom edge. */
  padding-bottom: 12px;
}
.ncardwrap:has(.cardmenu[open]) { z-index: 30; }

.ncard {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px 10px 16px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  color: inherit;
  text-decoration: none;
  transition: transform 0.12s ease;
}
a.ncard:active { transform: scale(0.975); }

.ncard .nrow__glyph {
  width: 28px;
  height: 28px;
  border-radius: 9px;
}
.ncard .nrow__glyph .nav__glyph { width: 16px; height: 16px; }

.ncard__text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.ncard__head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 28px;
}
.ncard .nrow__header {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
}
.ncard__time {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--liquid-tertiary-label);
}
.ncard .nrow__body {
  margin-top: 4px;
  font-size: 15px;
  line-height: 1.4;
  color: var(--liquid-primary-label);
}
.ncard .gallery--card { margin-top: 10px; }
/* RETIRED 2026-10-07 with the voice chip: .ncard .nrow__voice { margin-top: 10px; } */
.ncard .nrow__foot { margin-top: 6px; font-size: 13px; }
.ncard__action {
  align-self: flex-start;
  margin-top: 8px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}

/* READ STEPS BACK, the whole card at once: liquidReadDim is 0.62 of each
 * colour's own alpha. The header also loses its weight, as the app's does. */
.ncard--read .nrow__header,
.ncard--read .nrow__body,
.ncard--read .ncard__time,
.ncard--read .nrow__foot { opacity: 0.62; }
.ncard--read .nrow__header { font-weight: 500; }

/* The ⋮ sits over the time, top right; the time makes way while it shows. */
.ncardwrap > .cardmenu { position: absolute; top: 9px; right: 10px; }
@media (hover: hover) and (pointer: fine) {
  .ncardwrap:hover .ncard__time,
  .ncardwrap:focus-within .ncard__time,
  .ncardwrap:has(.cardmenu[open]) .ncard__time { visibility: hidden; }
}

/* The reaction pill rides the bottom edge, right (CardEdgeChips). */
.ncard__edge { position: absolute; right: 16px; bottom: 0; }

/* ══ MY PINGS, AT THE APP'S SHAPE — 2026-09-22 ═══════════════════════════ */

/* The ⋮ now shows only on hover, over the status mark in the corner, so the
 * head no longer reserves 34px for it; the mark makes way while it shows. */
@media (hover: hover) and (pointer: fine) {
  .acardwrap:hover .acard__status,
  .acardwrap:focus-within .acard__status,
  .acardwrap:has(.cardmenu[open]) .acard__status { visibility: hidden; }
}

/* "Next: in 2h", in the ping accent (_NextFireBadge). At the foot's leading
 * end since 2026-10-02, where a completed card says when it completed — so it
 * is an .acard__done too, and only the ink differs. It followed the schedule
 * on the header line, as `.acard__next { color; font-weight: 400 }`. */
/* 2026-10-05 (user): a clock in the accent and the time in the foot's grey,
 * the audience line's twin. It was the whole line in the accent:
 * .acard__done.acard__next { color: var(--acard-accent); } */
.acard__done.acard__next { display: inline-flex; align-items: center; gap: 5px; }
.acard__nextglyph { display: inline-flex; flex: 0 0 auto; color: var(--acard-accent); }
.acard__nextglyph .nav__glyph { width: 13px; height: 13px; }
/* PingCardHeader's secondary half — "Arrive • Office", "MrBeast • New video". */
.acard__sub { color: var(--liquid-secondary-label); font-weight: 400; }

/* THE FOOT: who it goes to, bottom right (PingAudienceLine), 13px in the
 * secondary ink with a small mark, as PingCardFootLabel draws it. */
.acard__foot { display: flex; justify-content: flex-end; gap: 12px; margin-top: -2px; }
/* WHEN IT COMPLETED, bottom left (_CompletedAt, 2026-10-01): the same 13px
 * secondary ink, in place of the check mark the status slot drew. */
.acard__done {
  margin-right: auto;
  min-width: 0;
  font-size: 13px;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* NEEDS ATTENTION, bottom left (_AttentionNotice, 2026-10-04): a ping Aletto
 * paused says so — "Sends to no one" when its circle or channel was deleted or
 * its last delivery stopped, "Paused by Aletto" otherwise. 13px w600 in the
 * caution amber (Palette.caution) behind the warning mark. */
.acard__done.acard__notice {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-weight: 600;
  color: #8F6500;
}
.acard__notice svg { flex: 0 0 13px; width: 13px; height: 13px; }
.acard__notice > span { overflow: hidden; text-overflow: ellipsis; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .acard__done.acard__notice { color: #F4C542; }
}
:root[data-theme="dark"] .acard__done.acard__notice { color: #F4C542; }
.acard__audience {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  font-size: 13px;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  white-space: nowrap;
}
/* SCROLLS SIDEWAYS, NOT CUT SHORT (2026-10-02, user): "To Ana, Ravi, Meera…"
 * hid the very names it exists to show. Only the text moves; the mark stays.
 * No bar drawn — a hairline under a 13px line reads as an underline. It was
 * `text-overflow: ellipsis` on .acard__audience. */
.acard__audiencetext {
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.acard__audiencetext::-webkit-scrollbar { display: none; }
.acard__audienceglyph { display: inline-flex; flex: 0 0 auto; }
.acard__audienceglyph .nav__glyph { width: 14px; height: 14px; }
.acard--muted .acard__foot { opacity: 0.62; }

/* The cards' vertical rhythm: 12px between them (LiquidSpace.stack). */
.acardwrap + .acardwrap { margin-top: 12px; }

/* MY PINGS' GROUPS (2026-09-28; sticky and capped 2026-10-01):
 * "Active · 8", "Paused · 2", "Completed · 14" — pings_screen.dart's
 * _GroupHeaderDelegate. Each group is a <section>, so its sticky head is held
 * by its own group and is pushed off by the next one's, as the app's pinned
 * headers are. A group after a card sits a section (24) below it. */
.pinggroup + .pinggroup,
.acardwrap + .pinggroup { margin-top: 24px; }

/* THE HEAD: the name chip, in the Inbox .daychip's measurements and material
 * — it rides over the cards, so it needs a fill. The row itself is
 * transparent to the pointer, so a card passing beside the chip can still be
 * pressed. ("Show less" on the right retired 2026-10-01, user.) */
.pinggroup__head {
  position: sticky;
  top: 8px;
  z-index: 5;
  display: flex;
  align-items: center;
  /* Was space-between, for "Show less" on the right; the count badge sits
   * beside the chip instead (2026-10-02). */
  justify-content: flex-start;
  gap: 6px;
  margin: 0 4px 12px;
  pointer-events: none;
}
.pinggroup__chip {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  min-width: 0;
  margin: 0;
  padding: 5px 10px;
  border-radius: 12px;
  background: var(--liquid-flat-chrome-fill);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.4px;
  line-height: 1.35;
  color: var(--liquid-secondary-label);
  white-space: nowrap;
}
/* THE COUNT, A BADGE BESIDE THE CHIP (2026-10-02, user): "Waiting · 1" read as
 * one word with a stray dot in it. The chip's material, a capsule (circular
 * ends) that is a circle at one digit, the number in the ping accent with
 * tabular figures so 9 → 10 does not jitter. The app's _GroupHeaderDelegate
 * badge. Hidden from screen readers, which hear the chip's "Waiting · 1". */
.pinggroup__count {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 11px;
  background: var(--liquid-flat-chrome-fill);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--liquid-ping-accent);
}
/* MY DEVICES' AND MY EMAILS' SECTIONS (2026-10-05): "Receiving pings" and
 * "Not receiving pings", the same head as a My Pings group — the app's
 * GroupCountHeader. Not sticky (a few cards, nothing to keep in view) and the
 * count in the brand, as the app draws it on those screens. Only rendered
 * when a page has both sections. */
.pinggroup--plain .pinggroup__head { position: static; }
.pinggroup--plain .pinggroup__count { color: var(--liquid-accent); }
/* RETIRED 2026-10-01 (user) — .pinggroup__less, "Show less": accent ink,
 * 4px gap, its 13px chevron and focus ring, shared with the chip above. */
.pinggroup__more .nav__glyph { width: 13px; height: 13px; }
.pinggroup__more:focus-visible { outline: 2px solid var(--liquid-ping-accent); outline-offset: 2px; }

/* THE CHIP FOLDS ITS GROUP (2026-10-01, the app's chevron): the whole chip is
 * the link, so the padding moves from the chip to it. The chevron is in the
 * ping accent, up while open and down while folded. */
.pinggroup__chip:has(> .pinggroup__fold) { padding: 0; }
.pinggroup__fold {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  padding: 5px 10px;
  border-radius: inherit;
  color: inherit;
  text-decoration: none;
}
.pinggroup__fold .nav__glyph { flex: 0 0 auto; width: 12px; height: 12px; color: var(--liquid-ping-accent); }
.pinggroup__fold:focus-visible { outline: 2px solid var(--liquid-ping-accent); outline-offset: 2px; }

/* Where "Show more" and the chip land: below the sticky head, not under
 * it. The anchor is an empty span before a page's first card, so the card
 * rhythm continues across it. */
.pinggroup,
.pinggroup__anchor { scroll-margin-top: 52px; }
.pinggroup__anchor { display: block; }
.pinggroup__anchor + .acardwrap { margin-top: 12px; }

/* "Show N more": a tinted capsule centred under the group, the app's
 * _showMoreSliver. Tinted, not filled — it ends a list rather than floating
 * over one, and three solid pills on one page would outshout the cards. */
.pinggroup__more {
  display: flex;
  width: max-content;
  align-items: center;
  gap: 7px;
  margin: 18px auto 6px;
  padding: 8px 16px;
  border-radius: var(--liquid-radius-capsule);
  background: color-mix(in srgb, var(--liquid-ping-accent) 10%, transparent);
  color: var(--liquid-ping-accent);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
}
:root[data-theme="dark"] .pinggroup__more { background: color-mix(in srgb, var(--liquid-ping-accent) 18%, transparent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pinggroup__more { background: color-mix(in srgb, var(--liquid-ping-accent) 18%, transparent); }
}

/* RETIRED 2026-10-01 — the Completed fold: .pinggroup as a <details>, its
 * summary losing the browser triangle, and .pinggroup__show / __hide
 * swapping on [open]. Nothing folds now; see the heads above. The
 * .pinggroup__head margin rule for a bare <h2> went with it. */

/* THE PLACE-REQUEST BANNER (PendingPlaceRequestsBanner): a card-radius row
 * with an emerald hairline at 45%, a faint fill, the place mark, a 15/600
 * sentence and a chevron. */
.reqbanner {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 12px;
  padding: 14px;
  border-radius: var(--liquid-radius-card);
  border: 1px solid rgba(12, 136, 99, 0.45);
  background: rgba(0, 0, 0, 0.03);
  color: var(--liquid-primary-label);
  text-decoration: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .reqbanner { background: rgba(255, 255, 255, 0.05); }
}
:root[data-theme="dark"] .reqbanner { background: rgba(255, 255, 255, 0.05); }
.reqbanner__glyph, .reqbanner__chev { display: inline-flex; flex: 0 0 auto; }
.reqbanner__glyph .nav__glyph { width: 20px; height: 20px; }
.reqbanner__chev { color: var(--liquid-tertiary-label); }
.reqbanner__chev .nav__glyph { width: 16px; height: 16px; }
.reqbanner__text { flex: 1 1 auto; font-size: 15px; font-weight: 600; }
/* One row per incoming request (2026-09-26): the place under the ask. */
.reqbanner__sub { display: block; margin-top: 2px; font-size: 13px; font-weight: 400; color: var(--liquid-secondary-label); }
.reqbanner + .reqbanner { margin-top: 8px; }

/* THE EMPTY STATE'S MARK (EmptyStateView's icon), shared with the Inbox. */
.emptystate__glyph { display: inline-flex; color: var(--liquid-tertiary-label); }
.emptystate__glyph .nav__glyph { width: 44px; height: 44px; stroke-width: 1.5; }

/* THE TWO WAYS TO START (_QuickStartOption): a card-radius row in the ping
 * accent at 7% with a 22% hairline, a 40px round glyph at 14%, a 15/600
 * title over a 13px italic example, and a chevron. */
.quickstart { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 440px; margin-top: 8px; }
.quickstart__row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-radius: var(--liquid-radius-card);
  border: 1px solid rgba(45, 79, 158, 0.22);
  background: rgba(45, 79, 158, 0.07);
  color: inherit;
  text-align: left;
  text-decoration: none;
}
.quickstart__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(45, 79, 158, 0.14);
  color: #2D4F9E;
}
.quickstart__glyph .nav__glyph { width: 20px; height: 20px; }
.quickstart__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.quickstart__title { font-size: 15px; font-weight: 600; color: var(--liquid-primary-label); }
.quickstart__example {
  font-size: 13px;
  font-style: italic;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.quickstart__chev { display: inline-flex; color: var(--liquid-tertiary-label); }
.quickstart__chev .nav__glyph { width: 16px; height: 16px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .quickstart__glyph { color: #7C9CFF; }
}
:root[data-theme="dark"] .quickstart__glyph { color: #7C9CFF; }

/* ══ A RECEIVED PING, OPENED — 2026-09-22 ════════════════════════════════
 * PingNotificationViewScreen: one panel holding the headline (20/700) and
 * the blocks, the reaction pill on its bottom edge, the ⋮ in its corner on
 * hover (press and hold on a phone), then From and Details under it. */
.rxwrap { position: relative; padding-bottom: 12px; margin-bottom: 12px; }
.rxwrap:has(.cardmenu[open]) { z-index: 30; }
.rxpanel {
  padding: 16px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
}
.rxpanel__head {
  margin: 0 36px 16px 0;
  font-size: 20px;
  line-height: 1.25;
  font-weight: 700;
  color: var(--liquid-primary-label);
}
.rxwrap > .cardmenu { position: absolute; top: 12px; right: 12px; }
.rxdetail { color: var(--liquid-secondary-label); }

/* ══ THE COMPOSER, AT THE APP'S SHAPE — 2026-09-22 ══════════════════════════
 * plain_ping_form_screen.dart: the ping (message + When), then Who gets
 * it with the tray at the foot. Blue is the ping accent throughout. */
.compose { --flow-accent: var(--liquid-ping-accent); display: block; }
.compose__pane { display: flex; flex-direction: column; gap: 8px; }
.compose__pane + .compose__pane { margin-top: 32px; }
.compose__pane[hidden] { display: none; }
.flowtitle {
  margin: 0 0 8px;
  font-size: 28px;
  line-height: 1.15;
  font-weight: 700;
  color: var(--liquid-primary-label);
}
.formlabel {
  margin: 16px 4px 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--liquid-secondary-label);
}

/* The composer: a card holding the words, the link when there is one, and
 * the attach row (PingComposer). */
.composer {
  display: flex;
  flex-direction: column;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  overflow: hidden;
}
.composer:focus-within { border-color: color-mix(in srgb, var(--flow-accent) 60%, transparent); }
.composer__text {
  min-height: 120px;
  padding: 14px 16px;
  border: 0;
  resize: vertical;
  background: transparent;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 17px;
  line-height: 1.4;
}
.composer__text:focus { outline: none; }
.composer__text::placeholder { color: var(--liquid-tertiary-label); }
.composer__link {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 12px 8px;
  padding: 8px 12px;
  border-radius: 12px;
  background: var(--liquid-fill-quiet);
}
.composer__link[hidden] { display: none; }
.composer__linkglyph .nav__glyph { width: 16px; height: 16px; color: var(--flow-accent); }
.composer__linkinput {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 15px;
}
.composer__linkinput:focus { outline: none; }

/* ATTACH (partial-composer-attach.html) — PingComposer's photo strip, the
 * voice-note row and the toolbar under its own heading. The strip is 150px
 * tall as kPingComposerStripHeight; a tile takes the photo's own shape, so
 * a portrait is not cropped to a letterbox while its author decides. */
.attach__strip {
  display: flex;
  gap: 6px;
  height: 150px;
  margin-top: 12px;
  overflow-x: auto;
  overflow-y: hidden;
}
.attach__strip[hidden] { display: none; }
.attach__tile {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  aspect-ratio: 1 / 1;
  max-width: 280px;
  border-radius: var(--liquid-radius-row);
  overflow: hidden;
  background: var(--liquid-fill-quiet);
}
.attach__tile[draggable="true"] { cursor: grab; }
/* RETIRED 2026-10-05 — .attach__tile--drag; every run drags with .attach--dragging. */
.attach__tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.attach__tile--pending { animation: attach-pulse 1.2s ease-in-out infinite; }
@keyframes attach-pulse { 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) {
  .attach__tile--pending { animation: none; }
}
.attach__remove {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: rgb(0 0 0 / 0.55);
  color: #fff;
  font-size: 13px;
  line-height: 26px;
  cursor: pointer;
}
/* ── RETIRED 2026-10-05 — .attach__voice, .attach__voicelabel, .attach__voice--pending ──
 * The composer's ONE voice-note row (label, player, ✕), from when a ping
 * held one. Several may be attached now, and they are rows of a run in the
 * reading surface's .attachrow shape (.attachlist--compose, at the end of
 * this file). Was: a flex row 12px gap, padding 10px 44px 10px 14px on the
 * content surface with a hairline, the ✕ centred on its right, the label 15px
 * 600, the player 36px tall, and a 36px pulsing pill while it uploaded. */
.attachbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.attachbar__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.attachbar__chips[hidden], .attachbar__note[hidden], .attachbar__rec[hidden], .attachchip[hidden] { display: none; }
.attachbar__note { margin: 0; font-size: 14px; color: var(--liquid-secondary-label); }
.attachbar__rec { display: flex; align-items: center; gap: 10px; }
.attachbar__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--liquid-destructive); }
.attachbar__elapsed { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--liquid-primary-label); }
.attach .formerror { margin-top: 8px; }
.attach .formerror[hidden] { display: none; }
.attachchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-fill-quiet);
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.attachchip__glyph .nav__glyph { width: 15px; height: 15px; color: var(--flow-accent); }

/* WHEN: the chips (_WhenChip) — a card-radius pill, 15/500, and when chosen
 * the accent at 10% with a 1.6px accent rim and bold ink. The radio inside
 * is the control; the pill is its label. */
.whenchips { display: flex; flex-wrap: wrap; gap: 8px; }
.whenchip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 14px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-separator);
  color: var(--liquid-primary-label);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
}
.whenchip input { position: absolute; opacity: 0; pointer-events: none; }
.whenchip:has(input:focus-visible) { outline: 2px solid var(--flow-accent); outline-offset: 2px; }
.whenchip:has(input:checked) {
  background: color-mix(in srgb, var(--flow-accent) 12%, var(--liquid-content-surface));
  border: 1.6px solid color-mix(in srgb, var(--flow-accent) 85%, transparent);
  color: var(--flow-accent);
  font-weight: 700;
}
.whenchip__glyph .nav__glyph { width: 14px; height: 14px; }
.whenpanel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 4px;
  padding: 12px 16px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
}
.whenpanel[hidden], .whenpanel__days[hidden] { display: none; }
.whenpanel__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 15px;
  color: var(--liquid-primary-label);
}
.whenpanel__row .input { max-width: 220px; }
.whenpanel__days .daybar__day { position: relative; cursor: pointer; }
.whenpanel__days input { position: absolute; opacity: 0; pointer-events: none; }

/* MORE OPTIONS (2026-10-13) — the app's _moreOptions: one quiet line, its
 * value on the right while folded, a chevron that turns when open. */
.whenpanel__more[hidden] { display: none; }
.whenpanel__more > summary { list-style: none; cursor: pointer; }
.whenpanel__more > summary::-webkit-details-marker { display: none; }
.whenpanel__moresum {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--liquid-secondary-label);
}
.whenpanel__moresum::after {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin: 0 4px 3px 0;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 180ms;
}
.whenpanel__more[open] > .whenpanel__moresum::after { transform: rotate(225deg); margin-bottom: -3px; }
.whenpanel__morevalue {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  font-size: 13px;
  font-weight: 500;
  color: var(--liquid-tertiary-label);
}
/* Open, the rows are the answer. */
.whenpanel__more[open] .whenpanel__morevalue { visibility: hidden; }
.whenpanel__more > .whenpanel__row { margin-top: 10px; }

/* THE FOOT OF THE PING PANE (2026-09-26): the Who pane's panel, as the app
 * docks both — the When (or the channel and trigger) read back on the left,
 * PingDockSummary, and the bold forward arrow in the square. It was a green
 * "Next ›" capsule. The tray rules below draw it; these are its own parts. */
.tray--dock { margin-top: 24px; }
.whendock {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  min-width: 0;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--liquid-primary-label);
}
.whendock__glyph { flex: 0 0 auto; display: inline-flex; color: var(--liquid-secondary-label); }
.whendock__glyph .nav__glyph { width: 18px; height: 18px; }
.whendock__text {
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.whendock--muted, .whendock--muted .whendock__glyph { color: var(--liquid-tertiary-label); }
/* Muted, still pressable, while nothing is chosen — the app's isReady. */
.tray:has(.whendock--muted) .tray__send {
  background: color-mix(in srgb, var(--flow-accent) 45%, var(--liquid-content-surface));
}
/* 'arrow.forward.bold' */
.tray__send--next .nav__glyph { stroke-width: 2.8; }

/* WHO GETS IT: back, title, search, the two groups, then everyone. */
/* ONE WAY BACK ON THE WHO PANE (2026-09-26). The pane's own chevron returns to
 * the form, as the app's leading does there; the page head's, drawn by backFor
 * on /pings/new/social, would leave the flow — two identical chevrons, one
 * above the other, meaning different things. Hidden, not removed, so the
 * centred title does not move. */
body:has(.compose__pane[data-pane="who"]:not([hidden])) .pagehead__back { visibility: hidden; }
.flowhead { display: flex; align-items: center; gap: 12px; }
.flowhead .flowtitle { margin: 0; }
.searchbar--flow { margin: 8px 0 4px; }
.targetgroups, .targetlist {
  display: flex;
  flex-direction: column;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  overflow: hidden;
}
.targetgroup + .targetgroup { border-top: var(--liquid-hairline) solid var(--liquid-separator); }
.targetgroup > summary { list-style: none; cursor: pointer; }
.targetgroup > summary::-webkit-details-marker { display: none; }
.targetgroup[open] .trow__chev { transform: rotate(90deg); }
.trow {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 8px 16px;
  cursor: pointer;
}
.trow[hidden] { display: none; }
.targetlist .trow + .trow,
.targetgroup > .trow { border-top: var(--liquid-hairline) solid var(--liquid-separator); }
.trow__disc {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: #fff;
  font-weight: 700;
  font-size: 16px;
}
.trow__disc--square { border-radius: 11px; }
.trow__disc--channel { border-radius: 11px; background: var(--liquid-channel-accent); }
.trow__disc--circle { background: var(--liquid-circle-accent); }
.trow__disc .nav__glyph { width: 20px; height: 20px; }
.trow__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.trow__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.trow__name {
  font-size: 16px;
  font-weight: 500;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.trow__sub { font-size: 13px; color: var(--liquid-secondary-label); }
.trow__chev { display: inline-flex; color: var(--liquid-tertiary-label); transition: transform 0.15s ease; }
.trow__chev .nav__glyph { width: 16px; height: 16px; }
/* The round check the app's rows carry, trailing. */
.trow__check {
  appearance: none;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  margin: 0;
  border-radius: 50%;
  border: 1.6px solid var(--liquid-tertiary-label);
  display: grid;
  place-content: center;
  cursor: pointer;
}
.trow__check:checked { background: var(--flow-accent); border-color: var(--flow-accent); }
.trow__check:checked::after {
  content: "";
  width: 6px;
  height: 11px;
  margin-top: -2px;
  border: solid #fff;
  border-width: 0 2.4px 2.4px 0;
  transform: rotate(45deg);
}
.trow__check:focus-visible { outline: 2px solid var(--flow-accent); outline-offset: 2px; }

/* THE TRAY (PingSendPanel): pinned to the foot of the viewport while the
 * picker is open — the chosen faces, then the price beside the button. */
.tray {
  position: sticky;
  bottom: 12px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 16px;
  padding: 12px 14px;
  border-radius: var(--liquid-radius-group);
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
/* ABOVE THE TAB BAR ON A PHONE (2026-09-26), at the offset .actbar uses: a
 * long pane — the social form, a long contact list — pinned the tray under
 * the floating tab bar and the ＋, hiding the square. */
/* 16 above the safe area since 2026-10-03: the forms that carry a tray are
 * pushed pages, and those draw no tab bar (see the phone block). */
@media (max-width: 700px) {
  .tray { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
  .app--tab .tray { bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
}
.tray__picks { display: flex; gap: 12px; overflow-x: auto; scrollbar-width: none; }
.tray__picks[hidden] { display: none; }
.tray__pick { flex: 0 0 auto; width: 56px; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.tray__face { width: 44px; height: 44px; }
.tray__name {
  max-width: 56px;
  font-size: 11px;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tray__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* The panel's left: who is chosen, or one quiet line saying nobody is yet
 * (pingSendPanelNoneYet), so the square never moves as the tray fills. */
.tray__lead { flex: 1; min-width: 0; min-height: 56px; display: flex; align-items: center; }
.tray__lead .tray__picks { flex: 1; min-width: 0; }
.tray__none { margin: 0; font-size: 14px; line-height: 1.3; color: var(--liquid-tertiary-label); }
.tray__none[hidden] { display: none; }
/* The price, centred above the controls, and gone while there is none (the
 * app's null cost collapses the slot). */
.tray__cost { display: flex; flex-direction: column; align-items: center; text-align: center; min-width: 0; }
.tray__cost:not(:has(*)) { display: none; }
.tray__price { font-size: 15px; font-weight: 600; color: var(--liquid-primary-label); }
.tray__avail { font-weight: 400; color: var(--liquid-secondary-label); }
.tray__warn { font-size: 13px; color: var(--liquid-destructive-label); }

/* THE SEND SQUARE (_SendSquare, 2026-09-26): a 56px rounded square in the
 * flow's accent with the glyph alone — send for Now, the calendar for a
 * schedule, the tick for a creator ping — or the word itself for an edit's
 * Save. The verb is the button's accessible name. It was a green capsule
 * reading "Send ➚", the one control in the flow that was not the app's. */
.tray__send {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  padding: 0 4px;
  border: 0;
  border-radius: 16px;
  background: var(--flow-accent);
  color: var(--liquid-on-ping-accent);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: transform 120ms ease;
}
.tray__send:active { transform: scale(0.94); }
/* Muted, still pressable, while nobody is chosen — _SendSquare's isReady:
 * the accent at 45% over the panel, and the press is what names the gap. */
.tray:has(.tray__none:not([hidden])) .tray__send {
  background: color-mix(in srgb, var(--flow-accent) 45%, var(--liquid-content-surface));
}
.tray__send:focus-visible { outline: 2px solid var(--flow-accent); outline-offset: 3px; }
.tray__send:disabled { opacity: 0.5; cursor: default; }
.tray__send .nav__glyph { width: 24px; height: 24px; stroke-width: 2.4; }
.tray__glyph { display: inline-flex; }
.tray__glyph[hidden] { display: none; }

/* ══ PEOPLE, AT THE APP'S SHAPE — 2026-09-22 ═══════════════════════════════
 * people_screen.dart: search, the pending band, circles, then My People as
 * flat rows (UserRowTile, flat) in up to three columns. */

/* The pending band (_InboxBand): one bordered card of one-line rows, the rim
 * in the caution amber because it holds a decision. */
.peopleband {
  margin: 0 0 24px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: 1px solid color-mix(in srgb, #9E6F00 38%, transparent);
  overflow: hidden;
}
.peopleband__row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  list-style: none;
  cursor: pointer;
}
.peopleband__row::-webkit-details-marker { display: none; }
.peopleband__label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 17px;
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.peopleband__chev { display: inline-flex; color: var(--liquid-secondary-label); transition: transform 0.15s ease; }
.peopleband__chev .nav__glyph { width: 16px; height: 16px; }
.peopleband[open] .peopleband__chev { transform: rotate(90deg); }
.peopleband__req {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  border-top: var(--liquid-hairline) solid var(--liquid-separator);
}
.peopleband__req .prow { flex: 1 1 auto; min-width: 0; padding: 0; }
.peopleband__verbs { display: flex; gap: 8px; flex: 0 0 auto; }

/* The face pile (_FacePile): 24px faces stepped 15px, each in a request-blue
 * ring with a card-coloured gap. Held in a 54px slot like the app's. */
.facepile { position: relative; display: inline-flex; flex: 0 0 54px; height: 24px; }
.facepile__face {
  position: relative;
  width: 24px;
  height: 24px;
  margin-left: -9px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  box-shadow: 0 0 0 1.5px var(--liquid-content-surface), 0 0 0 3px #5374CC;
}
.facepile__face:first-child { margin-left: 0; z-index: 3; }
.facepile__face:nth-child(2) { z-index: 2; }

/* My People: flat rows in a grid — 44px face, 15/600 name, 13 handle, the
 * status badge trailing. One column on a phone, up to three when wide. */
.peoplegrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  column-gap: 20px;
}
.peoplegrid > .empty { grid-column: 1 / -1; }
.prow {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  padding: 8px 4px;
  color: inherit;
  text-decoration: none;
  border-radius: var(--liquid-radius-row);
}
.prow:hover { background: var(--liquid-fill-quiet); }
/* THE ⋯ ON A PERSON ROW (2026-09-24) — ConnectionGridCard's press-and-hold
 * menu. The row is a line, not a card, so the button is centred on it rather
 * than pinned to a card's top corner, and the row keeps room for it. */
.prowwrap > .cardmenu { top: 50%; right: 4px; transform: translateY(-50%); }
.prowwrap > .prow { padding-right: 40px; }
.prow__face {
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: #fff;
  font-size: 17px;
  font-weight: 700;
}
.prow__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.prow__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.prow__name, .prow__handle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prow__name { font-size: 15px; font-weight: 600; color: var(--liquid-primary-label); }
.prow__handle { font-size: 13px; color: var(--liquid-secondary-label); }

/* UserStatusBadge: 32px; filled blue with a soft shadow for a request, an
 * outline glyph for "asked" (amber) and "add" (secondary). */
.prow__badge {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.prow__badge .nav__glyph { width: 18px; height: 18px; }
.prow__badge--request { background: #5374CC; color: #fff; box-shadow: 0 2px 6px color-mix(in srgb, #5374CC 35%, transparent); }
.prow__badge--asked { color: #9E6F00; }
.prow__badge--add { color: var(--liquid-secondary-label); }

/* The archive link beside My People carries the app's chevron. */
.group__action { display: inline-flex; align-items: center; gap: 2px; }
.group__actionchev { display: inline-flex; }
.group__actionchev .nav__glyph { width: 12px; height: 12px; }

/* ══ STEADY NAVIGATION — 2026-09-22 ════════════════════════════════════════
 *
 * THE COLUMN NO LONGER MOVES SIDEWAYS. The shell is centred, so the window's
 * vertical bar appearing (a short page growing past the window as pictures and
 * updates load) pushed every column left by half its width. The gutter is now
 * always reserved, bar or no bar. */
html { scrollbar-gutter: stable; }

/* A LINK NO LONGER BLANKS THE PAGE. Same-origin navigations cross-fade the
 * centre column, and the rails carry their own transition names, so they are
 * matched old-to-new and hold still instead of flashing. Browsers without
 * cross-document view transitions navigate as before. */
@view-transition { navigation: auto; }
.nav { view-transition-name: nav; }
.rail { view-transition-name: rail; }
::view-transition-group(root) { animation-duration: 0.14s; }
::view-transition-group(nav),
::view-transition-group(rail) { animation: none; }
::view-transition-old(nav),
::view-transition-old(rail) { display: none; }
::view-transition-new(nav),
::view-transition-new(rail) { animation: none; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* STRIPS GLIDE UNDER A POINTER. Snapping is kept for touch, where a flick
 * settling on a tile is how a row of targets should feel; with a wheel or a
 * trackpad it turned every notch into a jump to the next tile. layout.html
 * animates the wheel's sideways scroll. */
@media (hover: hover) and (pointer: fine) {
  [data-hscroll] { scroll-snap-type: none; }
}

/* ══ THE YOUTUBE / TWITCH FORM — 2026-09-22 ═══════════════════════════════
 * creator_ping_form_screen.dart: the platform strip, the channel row with the
 * lookup under it, "Tell me when" inline, the milestone choice, then Who.
 * .compose supplies the panes and the ping accent (kSocialPrimary IS the
 * ping accent); only the social pieces are here. */

/* The platform strip (_ProviderTile): a 64px rounded square with the brand's
 * official full-colour logo, the name under it. The logos carry their own
 * fills (partial-icons), so nothing here colours them. Chosen, the square
 * takes a 2px ring in the tick badge's colour — drawn as border plus an inset
 * shadow so the tile does not shift — and the badge on the bottom trailing
 * corner fills green; unchosen it is an empty ring.
 *
 * ── RETIRED 2026-09-29 — the platform-colour fill ──
 * .providertile { --provider: #FF0033 }, .providertile--twitch { --provider:
 * #9146FF }, the square's `color: var(--provider)`, and when checked
 * `background: var(--provider); color: #fff`. It knocked the logo out white
 * on its own colour; both brands forbid recolouring their logos. */
.providerstrip { display: flex; gap: 12px; padding: 0 4px 6px; overflow-x: auto; }
.providertile {
  position: relative;
  flex: 0 0 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}
.providertile input { position: absolute; opacity: 0; pointer-events: none; }
.providertile__square {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: 18px;
  display: grid;
  place-items: center;
  background: var(--liquid-fill-quiet);
  border: 1px solid var(--liquid-content-border);
  transition: border-color 180ms ease, box-shadow 180ms ease;
}
.providertile__square > .nav__glyph { width: 30px; height: 30px; }
.providertile__badge {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--liquid-grouped-background);
  border: 1.5px solid var(--liquid-tertiary-label);
  color: transparent;
}
.providertile__badge .nav__glyph { width: 12px; height: 12px; stroke-width: 3; }
.providertile__label {
  font-size: 12px;
  font-weight: 500;
  color: var(--liquid-secondary-label);
  white-space: nowrap;
}
.providertile:has(input:checked) .providertile__square {
  border-color: var(--liquid-primary-action);
  box-shadow: inset 0 0 0 1px var(--liquid-primary-action);
}
.providertile:has(input:checked) .providertile__badge {
  background: var(--liquid-primary-action);
  border: 2px solid var(--liquid-grouped-background);
  color: #fff;
}
.providertile:has(input:checked) .providertile__label { font-weight: 600; color: var(--liquid-primary-label); }
.providertile:has(input:focus-visible) .providertile__square { outline: 2px solid var(--flow-accent); outline-offset: 3px; }

/* The channel row (FormSummaryRow): the bare mark,
 * the channel or "No channel chosen yet", and Search / Change. The mark is
 * the play glyph in the flow accent since 2026-09-29.
 *
 * ── RETIRED 2026-09-29 — .providermark--youtube { color: #FF0033 } and
 * .providermark--twitch { color: #9146FF }: the platform's logo tinted its
 * brand colour, a single-colour copy the brands do not allow. The logos are
 * drawn only in the platform strip now. */
.summaryrow {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 56px;
  padding: 12px 16px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.summaryrow__lead { display: inline-flex; flex: 0 0 auto; }
.summaryrow__lead .nav__glyph { width: 24px; height: 24px; }
.summaryrow__lead--accent { color: var(--flow-accent); }
.summaryrow__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 17px;
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.summaryrow__title--muted { font-weight: 500; color: var(--liquid-secondary-label); }
.summaryrow__action { flex: 0 0 auto; font-size: 15px; font-weight: 500; color: var(--flow-accent); }
.summaryrow__action--strong { font-weight: 700; }

/* The lookup (the app's channel sheet, inline): the field and Find channel on
 * one line, the answer as a row under them. */
.socialsearch { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.socialsearch[hidden] { display: none; }
.socialsearch__row { display: flex; gap: 8px; align-items: center; }
.socialsearch__input { min-height: 48px; }
.socialsearch__find { flex: 0 0 auto; min-height: 48px; }
.socialsearch__error { margin: 0 4px; }
.channelresult { display: flex; align-items: center; gap: 14px; padding: 10px 4px; }
.channelresult__thumb {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--flow-accent) 12%, transparent);
  color: var(--flow-accent);
}
.channelresult__thumb img { width: 100%; height: 100%; object-fit: cover; }
.channelresult__thumb .nav__glyph { width: 22px; height: 22px; }
.channelresult__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.channelresult__name { font-size: 16px; font-weight: 600; color: var(--liquid-primary-label); overflow: hidden; text-overflow: ellipsis; }
.channelresult__sub { font-size: 13px; color: var(--liquid-secondary-label); }
/* The flow's one "chosen" mark — the filled green ring (selectionTickColor). */
.channelresult__tick {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--liquid-accent);
  color: #fff;
}
.channelresult__tick .nav__glyph { width: 13px; height: 13px; stroke-width: 3; }

/* TELL ME WHEN (_TriggerRow): one card of rows — glyph, label over its
 * description, the round check trailing; the divider clears the glyph. The
 * chosen row's check is the selection green, as the app's SelectionCircle. */
.triggerlist {
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
  overflow: hidden;
}
.triggerrow { position: relative; display: flex; align-items: center; gap: 14px; padding: 12px 16px; cursor: pointer; }
.triggerrow[hidden] { display: none; }
.triggerrow:not([hidden]) ~ .triggerrow:not([hidden])::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50px;
  right: 16px;
  border-top: var(--liquid-hairline) solid var(--liquid-separator);
}
.triggerrow:hover { background: var(--liquid-fill-quiet); }
.triggerrow__glyph { display: inline-flex; flex: 0 0 auto; color: var(--liquid-secondary-label); }
.triggerrow__glyph .nav__glyph { width: 20px; height: 20px; }
.triggerrow:has(input:checked) .triggerrow__glyph { color: var(--flow-accent); }
.triggerrow__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.triggerrow__label { font-size: 16px; font-weight: 500; color: var(--liquid-primary-label); }
.triggerrow:has(input:checked) .triggerrow__label { font-weight: 600; }
.triggerrow__desc { font-size: 13px; line-height: 1.3; color: var(--liquid-secondary-label); }
.socialform .triggerrow .trow__check:checked { background: var(--liquid-accent); border-color: var(--liquid-accent); }

/* The milestone choice: the segmented control, then the step chips or the
 * numbers editor. */
.milestones { display: flex; flex-direction: column; gap: 8px; }
.milestones[hidden], [data-mode-panel][hidden], .formnote[hidden] { display: none; }
.seg--form { margin: 0; }
.seg--form .seg__btn { position: relative; }
.seg--form .seg__btn input { position: absolute; opacity: 0; pointer-events: none; }
.seg--form .seg__btn:has(input:focus-visible) { outline: 2px solid var(--flow-accent); outline-offset: 1px; }
.whenchip--locked { font: inherit; font-size: 15px; font-weight: 500; color: var(--liquid-tertiary-label); }
.whenchip--locked .nav__glyph { width: 12px; height: 12px; }
.milestoneat { display: flex; flex-direction: column; gap: 10px; }
.milestoneat__row { display: flex; gap: 10px; align-items: center; }
.milestoneat__add {
  flex: 0 0 auto;
  background: color-mix(in srgb, var(--flow-accent) 14%, transparent);
  color: var(--flow-accent);
}
.milestoneat__add .nav__glyph { width: 14px; height: 14px; }
.milestoneat__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.milestonechip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px 6px 14px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-separator);
  font-size: 13px;
  font-weight: 600;
  color: var(--liquid-primary-label);
}
.milestonechip__remove {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--liquid-tertiary-label);
  cursor: pointer;
}
.milestonechip__remove .nav__glyph { width: 14px; height: 14px; }
.milestonechip__remove:hover { background: var(--liquid-fill-quiet); }

/* The foot notes (_FootNote): 12/500, tertiary. The lock note is the web's
 * answer where the app raises a toast — the same sentence, in place. */
.formnote { margin: 4px 4px 0; font-size: 12px; line-height: 1.35; font-weight: 500; color: var(--liquid-tertiary-label); }
.formnote--lock { font-size: 13px; color: var(--liquid-secondary-label); }

/* ══ THE PAGE HEAD'S OWN BUTTONS, AND THE UPDATE PANEL — 2026-09-22 ═════════
 * .pagehead__tools holds what a page defines as "pageactions": the app's
 * LiquidAppBar.actions (flag, plus, trash). Unlike .pagehead__actions (the
 * account chips, carried by the rail above 700px) these belong to the page and
 * show at every width. Empty on most pages, and :empty takes it out of the
 * row so it adds no gap. */
.pagehead__tools {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 3px;
}
.pagehead__tools:empty { display: none; }
/* THE APP BAR'S ⋮ (2026-09-24): a card menu whose button is a chip, so it
 * sits in the row like the flag and the plus. Always drawn — it is not inside
 * a [data-card-menu] card, so neither the hover fade nor the touch hide
 * applies — and its panel drops from the chip's right edge. */
.cardmenu--appbar > summary.chipbtn { cursor: pointer; }
.cardmenu--appbar[open] > summary.chipbtn { background: var(--liquid-fill-quiet); }
.cardmenu--appbar .cardmenu__panel { gap: 2px; padding: 6px; text-align: left; }
.pagehead__form { display: contents; }
button.chipbtn { padding: 0; font: inherit; cursor: pointer; }
.chipbtn--destructive { color: var(--liquid-destructive); }

/* THE UPDATE, OPENED (channel_post_screen.dart): the list card's shell with
 * the byline carrying the date and time, and the words at reading size. The
 * pill hangs on the edge as it does on the card, so the section after it is
 * pushed clear of it. */
.pcard--detail { gap: 16px; margin-bottom: 40px; }
.pcard--detail .blocks p { font-size: 16px; line-height: 1.5; }
.pcard--detail .pcard__head .pcard__handle { font-size: 13px; }
.pcard--detail .blocks p { font-weight: 400; }
/* DETAILS: the label and the value at 13, the value in the label colour at
 * 600 — the app's row, where .row__value is sized for a settings value. */
.detailsrow .row__subtitle { font-size: 13px; }
.detailsrow .row__value { font-size: 13px; font-weight: 600; color: var(--liquid-primary-label); }

/* ── ADD PEOPLE — 2026-09-23 ─────────────────────────────────────────────────
 *
 * The invite menu is a page action (the app bar's ⋯) whose panel is the card
 * menu's; a stranger's row is a form so its add mark can be the request; the
 * share panel is a <dialog> like the lightbox and the sign-in panel. */
.pagemenu { position: relative; }
.pagemenu > summary { list-style: none; }
.pagemenu > summary::-webkit-details-marker { display: none; }
.pagemenu[open] > summary { background: var(--liquid-fill-quiet); }
.pagemenu__panel form { margin: 0; }
.cardmenu__item--icon { display: flex; align-items: center; gap: 10px; }
.cardmenu__item--icon .nav__glyph { width: 20px; height: 20px; flex: 0 0 auto; }

.btn--icon { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.btn--icon .nav__glyph { width: 18px; height: 18px; }
.emptystate form { margin: 6px 0 0; }
.emptystate--inline { padding-top: 8px; padding-bottom: 8px; }

.prow--form { margin: 0; }
.prow--form:hover { background: transparent; }
.prow__link {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  color: inherit;
  text-decoration: none;
}
button.prow__badge { border: 0; background: transparent; padding: 0; font: inherit; cursor: pointer; }
button.prow__badge--add:hover { background: var(--liquid-fill-quiet); color: var(--liquid-primary-action); }

/* The share panel — the app's form sheet: 20/700 title, the reward in the
 * emerald at 600, the sentence, the two verbs. */
.invitedlg {
  width: min(440px, calc(100vw - 32px));
  padding: 20px;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-overlay, 28px);
  background: var(--liquid-content-surface);
  color: var(--liquid-primary-label);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.24);
}
.invitedlg::backdrop { background: rgba(0, 0, 0, 0.36); }
.invitedlg__title { margin: 0 0 10px; font-size: 20px; font-weight: 700; }
.invitedlg__reward { margin: 0 0 14px; font-size: 15px; line-height: 1.4; font-weight: 600; color: var(--liquid-primary-action); }
.invitedlg__text { width: 100%; resize: none; font-size: 15px; line-height: 1.4; }
.invitedlg__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; }
.invitedlg__status { font-size: 13px; color: var(--liquid-secondary-label); }
.invitedlg__close { display: flex; justify-content: flex-end; margin: 12px 0 0; }
.invitedlg__body { margin: 0 0 14px; font-size: 15px; line-height: 1.4; color: var(--liquid-secondary-label); }
.invitedlg .input[type=email] { width: 100%; }
.invitedlg__status--error { color: var(--liquid-destructive); }
#invite-email-status .invitedlg__status { margin: 8px 0 0; }

/* The app's LiquidSpace.section between MY PEOPLE and EVERYONE ON ALETTO. */
.peoplegrid + .group__header { margin-top: 24px; }

/* INVITE A FRIEND on Credits (credits_screen.dart _InviteRewardCard): a store
 * panel with the emerald accent, 14 padding, a 44px emerald square carrying the
 * share glyph in white, the title at 16/600 and the note at 13. */
.rewardcard__form { margin: 0 0 24px; }
.rewardcard {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--liquid-primary-action) 45%, transparent);
  border-radius: var(--liquid-radius-card, 20px);
  background: color-mix(in srgb, var(--liquid-primary-action) 8%, var(--liquid-content-surface));
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.rewardcard:hover { background: color-mix(in srgb, var(--liquid-primary-action) 14%, var(--liquid-content-surface)); }
.rewardcard__mark {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: var(--liquid-radius-inner, 14px);
  background: var(--liquid-primary-action);
  color: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.rewardcard__mark .nav__glyph { width: 22px; height: 22px; }
.rewardcard__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.rewardcard__title { font-size: 16px; font-weight: 600; color: var(--liquid-primary-label); }
.rewardcard__body { font-size: 13px; line-height: 1.35; color: var(--liquid-secondary-label); }
.rewardcard__chev { flex: 0 0 auto; display: inline-flex; color: var(--liquid-secondary-label); }
.rewardcard__chev .nav__glyph { width: 16px; height: 16px; }

/* A person in the FROM row is round (2026-09-23); a channel keeps 12px. */
.pingfrom__avatar--round { border-radius: 50%; }

/* ══ THE PERSON PAGE, AS THE APP DRAWS IT (2026-09-23) ═══════════════════════
 *
 * connection_details_screen.dart: Profile's head (photo left, identity right),
 * the stats card, the two PERMISSIONS cards, their delivery strips and the
 * device picker. Every colour is palette.dart's:
 *   Incoming arrow  Palette.brandMid  #0C8863   (kConnectionInboundColor)
 *   Outgoing arrow  Palette.pingMid  #5374CC   (kConnectionOutboundColor)
 *   Delivery tile   Palette.brand when on, ink2 idle (no method colour since
 *                   2026-09-28 — push was premium violet, email ping blue)
 *   Tile well       Palette.well      = --liquid-fill-quiet
 *   Count badge     Palette.urgent    = --liquid-destructive-icon
 *   Item note       Palette.caution   #8F6500 / #F4C542
 *   The @ mark      Palette.brand     = --liquid-accent */

/* ProfileSquarePhoto: 112 square, radius 28, a 3pt ring and a soft shadow. */
.phero {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 8px 4px 24px;
}
.phero__photo {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 112px;
  height: 112px;
  border-radius: 28px;
  border: 3px solid #FFFFFF;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.10);
  color: #FFFFFF;
  font-size: 42px;
  font-weight: 600;
  overflow: hidden;
}
.phero__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.phero__id { min-width: 0; }
.phero__name {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.5px;
  line-height: 1.15;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.phero__handle {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 6px 0 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--liquid-secondary-label);
  min-width: 0;
}
.phero__handle .nav__glyph { width: 14px; height: 14px; flex: 0 0 auto; color: var(--liquid-accent); }
.phero__handle span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .phero__photo { border-color: rgba(255, 255, 255, 0.18); }
}
:root[data-theme="dark"] .phero__photo { border-color: rgba(255, 255, 255, 0.18); }

/* ProfileStats: one card, two figures, a 32pt rule between. */
.pstats {
  display: flex;
  align-items: center;
  margin: 0 0 24px;
  padding: 16px 0;
  border: var(--liquid-hairline) solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
  background: var(--liquid-content-surface);
}
.pstats__item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.pstats__n { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 28px; }
.pstats__label { font-size: 12px; color: var(--liquid-secondary-label); }
.pstats__rule { width: 1px; height: 32px; background: var(--liquid-separator); }

/* DirectionalTileCard: the head is a sentence — label, face, arrow, face — and
 * the body sits on a faint band under a hairline. Two across only where each
 * column gets 420px (the app's responsiveColumnCount rule), stacked below. */
.dcards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); gap: 24px 16px; align-items: start; margin-bottom: 24px; }
.dcard {
  border: var(--liquid-hairline) solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
  background: var(--liquid-content-surface);
  overflow: hidden;
}
.dcard--blocked { margin-bottom: 24px; }
.dcard__head { display: flex; align-items: center; padding: 16px; }
.dcard__head--center { justify-content: center; }
.dcard__label { font-size: 15px; font-weight: 700; letter-spacing: -0.1px; margin-right: 14px; }
.dcard__arrow { display: inline-flex; margin: 0 6px; color: rgba(12, 136, 99, 0.75); }
.dcard__arrow .nav__glyph { width: 18px; height: 18px; }
.dcard--out .dcard__arrow { color: rgba(83, 116, 204, 0.75); }
.dcard__arrow--blocked { margin: 0 10px; color: rgba(212, 66, 84, 0.75); }
.dcard__arrow--blocked .nav__glyph { width: 20px; height: 20px; }
.dcard__body {
  padding: 14px 16px 16px;
  background: rgba(0, 0, 0, 0.03);
  border-top: 1px solid rgba(0, 0, 0, 0.06);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dcard__body { background: rgba(0, 0, 0, 0.06); border-top-color: rgba(255, 255, 255, 0.07); }
  :root:not([data-theme="light"]) .dcard__arrow { color: rgba(12, 136, 99, 0.9); }
  :root:not([data-theme="light"]) .dcard--out .dcard__arrow { color: rgba(83, 116, 204, 0.9); }
  :root:not([data-theme="light"]) .dcard__arrow--blocked { color: rgba(212, 66, 84, 0.9); }
}
:root[data-theme="dark"] .dcard__body { background: rgba(0, 0, 0, 0.06); border-top-color: rgba(255, 255, 255, 0.07); }
:root[data-theme="dark"] .dcard__arrow { color: rgba(12, 136, 99, 0.9); }
:root[data-theme="dark"] .dcard--out .dcard__arrow { color: rgba(83, 116, 204, 0.9); }
:root[data-theme="dark"] .dcard__arrow--blocked { color: rgba(212, 66, 84, 0.9); }
.dcard__text { margin: 0; font-size: 13px; line-height: 1.35; text-align: center; color: var(--liquid-secondary-label); }
.dcard__text--start { text-align: start; }
.dcard__explain { margin: 0; font-size: 12px; line-height: 1.35; color: var(--liquid-secondary-label); }
.dcard__hint { margin: 0 0 12px; font-size: 12px; line-height: 1.35; text-align: center; color: var(--liquid-secondary-label); }
.dcard__note { margin: 12px 0 0; font-size: 12px; line-height: 1.35; text-align: center; color: var(--liquid-secondary-label); }
.dcard__actions { display: flex; justify-content: flex-end; margin: 12px 0 0; }

/* A face on a card head: AvatarUtils at radius 19. */
.dface {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  color: #FFFFFF;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
}
.dface__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* ActionButton: a filled capsule in the card's accent, the label white. */
.abtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 8px 16px;
  border: 0;
  border-radius: 999px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: #FFFFFF;
  cursor: pointer;
}
.abtn .nav__glyph { width: 16px; height: 16px; }
.abtn--brand { background: var(--liquid-primary-action); }
.abtn--destructive { background: var(--liquid-destructive); }
.abtn:hover { filter: brightness(1.06); }
.abtn:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }

/* ApproveRejectCard: Decline and Approve as two tinted cards, Ignore under. */
.dcard__pair { display: flex; gap: 10px; margin-top: 14px; }
.dcard__pair form { flex: 1; margin: 0; }
.pairbtn {
  --pair: #0C8863;
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--pair) 25%, transparent);
  border-radius: var(--liquid-radius-card, 20px);
  background: color-mix(in srgb, var(--pair) 7%, transparent);
  color: var(--pair);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.pairbtn .nav__glyph { width: 15px; height: 15px; }
.pairbtn--decline { --pair: var(--liquid-destructive); }
.pairbtn:hover { background: color-mix(in srgb, var(--pair) 12%, transparent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pairbtn { border-color: color-mix(in srgb, var(--pair) 35%, transparent); background: color-mix(in srgb, var(--pair) 10%, transparent); }
}
:root[data-theme="dark"] .pairbtn { border-color: color-mix(in srgb, var(--pair) 35%, transparent); background: color-mix(in srgb, var(--pair) 10%, transparent); }
.dcard__ignore { display: flex; justify-content: center; margin: 8px 0 0; }
.linkbtn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 8px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  font-weight: 500;
  color: var(--liquid-secondary-label);
  cursor: pointer;
}
.linkbtn .nav__glyph { width: 13px; height: 13px; color: var(--liquid-tertiary-label); }
.linkbtn:hover span { text-decoration: underline; }

/* ── RETIRED 2026-10-07 — .dstrip, .dtile, .dtile__form, .dtile__sq,
 * .dtile--on, .dtile--empty, .dtile__cap, .dtile__tick, .dtile__count: the
 * connection card's strips of tiles (SelectionStripTile). The card is rows
 * with switches now — see .connrow. The rules were (comments as (* *)):
 *
 * (* SelectionStripTile: a 56 squircle in a 64 column, the caption under it, a
 *  * tick bottom-right when selected and a count top-right. *)
 * .dstrip { display: flex; justify-content: center; gap: 10px; }
 * .dtile__form { margin: 0; }
 * .dtile {
 *   --dtile: var(--liquid-accent);
 *   display: flex;
 *   flex-direction: column;
 *   align-items: center;
 *   gap: 6px;
 *   width: 64px;
 *   padding: 0;
 *   border: 0;
 *   background: none;
 *   font: inherit;
 *   color: inherit;
 *   cursor: pointer;
 * }
 * 
 * .dtile__sq {
 *   position: relative;
 *   display: flex;
 *   align-items: center;
 *   justify-content: center;
 *   width: 56px;
 *   height: 56px;
 *   border-radius: 16px;
 *   border: 1px solid var(--liquid-content-border);
 *   background: var(--liquid-fill-quiet);
 *   color: var(--liquid-secondary-label);
 *   transition: background-color 180ms, border-color 180ms;
 * }
 * .dtile__sq > .nav__glyph { width: 22px; height: 22px; }
 * .dtile--on .dtile__sq {
 *   border: 1.5px solid color-mix(in srgb, var(--dtile) 55%, transparent);
 *   background: color-mix(in srgb, var(--dtile) 14%, var(--liquid-fill-quiet));
 *   color: var(--dtile);
 * }
 * .dtile--empty .dtile__sq { opacity: 0.4; }
 * .dtile__cap { font-size: 11px; line-height: 1.15; font-weight: 500; color: var(--liquid-secondary-label); }
 * .dtile--on .dtile__cap { color: var(--liquid-primary-label); }
 * .dtile:focus-visible .dtile__sq { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
 * .dtile:active .dtile__sq { transform: scale(0.96); }
 * .dtile__tick {
 *   position: absolute;
 *   right: -5px;
 *   bottom: -5px;
 *   display: flex;
 *   align-items: center;
 *   justify-content: center;
 *   width: 20px;
 *   height: 20px;
 *   border-radius: 50%;
 *   background: var(--dtile);
 *   border: 1.5px solid var(--liquid-content-surface);
 *   color: #FFFFFF;
 * }
 * @media (prefers-color-scheme: dark) {
 *   :root:not([data-theme="light"]) .dtile__tick { color: #06140E; }
 * }
 * :root[data-theme="dark"] .dtile__tick { color: #06140E; }
 * .dtile__tick .nav__glyph { width: 11px; height: 11px; stroke-width: 3; }
 * .dtile__count {
 *   position: absolute;
 *   right: -4px;
 *   top: -4px;
 *   min-width: 18px;
 *   height: 18px;
 *   padding: 0 5px;
 *   box-sizing: border-box;
 *   display: flex;
 *   align-items: center;
 *   justify-content: center;
 *   border-radius: 9px;
 *   background: var(--liquid-destructive-icon);
 *   border: 1.2px solid var(--liquid-content-surface);
 *   color: #FFFFFF;
 *   font-size: 11px;
 *   font-weight: 600;
 *   line-height: 1.1;
 * }
 */

/* ── A CONNECTION'S ROWS (2026-10-07) — "Get their pings on App", "Send them
 * my pings by Email": a list row each, the switch on the right. An incoming
 * row's label opens its picker, the count of chosen devices or addresses
 * under it — grey with the switch off, the caution colour with none. */
.connrows { margin: 0 -14px; }
.connrow__open {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.connrow__count {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 18px;
  margin-top: 6px;
  padding: 0 0 0 6px;
  border-radius: 9px;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  color: #fff;
  background: var(--liquid-selected-ink);
}
.connrow__count--off { background: var(--liquid-secondary-label); }
.connrow__count--warn { background: #9E6F00; }
.connrow__chev { display: inline-flex; width: 10px; height: 10px; margin-right: 4px; }
.connrow__chev .nav__glyph { width: 10px; height: 10px; }
.dpicker__hint--warn { color: #9E6F00; }

/* THE PICKER: StripSheetPanel as a dialog — title, the sentence, the item
 * rows on the well, and Done. */
.dpicker {
  width: min(440px, calc(100vw - 32px));
  padding: 20px;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-overlay, 28px);
  background: var(--liquid-content-surface);
  color: var(--liquid-primary-label);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.24);
}
.dpicker::backdrop { background: rgba(0, 0, 0, 0.36); }
.dpicker form { margin: 0; }
.dpicker__title { margin: 0 0 6px; font-size: 20px; font-weight: 700; }
.dpicker__sub { margin: 0 0 16px; font-size: 14px; line-height: 1.4; color: var(--liquid-secondary-label); }
.dpicker__list { display: flex; flex-direction: column; gap: 8px; max-height: 45vh; overflow-y: auto; }
.dpicker__cost { margin: 14px 0 0; font-size: 12px; line-height: 1.4; color: var(--liquid-secondary-label); }
.dpicker__actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.ditem {
  --ditem: var(--liquid-accent);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--liquid-content-border);
  border-radius: 14px;
  background: var(--liquid-fill-quiet);
  cursor: pointer;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ditem__note { color: #F4C542; }
}
:root[data-theme="dark"] .ditem__note { color: #F4C542; }
.ditem__glyph { display: inline-flex; color: var(--liquid-secondary-label); }
.ditem__glyph .nav__glyph { width: 20px; height: 20px; }
.ditem__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ditem__title { font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ditem__note { font-size: 12px; line-height: 1.3; color: #8F6500; }
.ditem input.ditem__check { flex: 0 0 auto; accent-color: var(--ditem); }

/* THE SWITCH — CupertinoSwitch at the app's 51×22 fitted size, emerald on. */
.row__switch { margin: 0 0 0 auto; flex: 0 0 auto; }
.switch {
  position: relative;
  width: 44px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 13px;
  background: rgba(0, 0, 0, 0.12);
  cursor: pointer;
  transition: background-color 180ms;
}
.switch__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  transition: transform 180ms;
}
.switch--on { background: var(--liquid-primary-action); }
.switch--on .switch__knob { transform: translateX(18px); }
.switch:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .switch:not(.switch--on) { background: rgba(255, 255, 255, 0.24); }
}
:root[data-theme="dark"] .switch:not(.switch--on) { background: rgba(255, 255, 255, 0.24); }

/* Pings from @x: the Inbox's accent on the row chip, and a two-line clamp. */
.row__icon--acard { --tint: var(--acard-accent, var(--liquid-secondary-label)); }
.row__title--clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.emptystate--muted .emptystate__glyph .nav__glyph { width: 32px; height: 32px; }
.emptystate--muted .emptystate__body { color: var(--liquid-tertiary-label); }

/* ── CHANNEL DETAIL — channel_detail_sections.dart (2026-09-23) ──────────────
 *
 * The identity card is a LiquidContentSurface at LiquidRadius.card with the
 * content hairline (channelCardBorder), padded 16: a 56px ChannelAvatar
 * (radius size × 0.28), the name at 22/700 on one line, the handle at 13; the
 * description at 15/1.45; followers and updates (17/700 over 12) left of the
 * reaction pill; then the notices — caution amber is Palette.caution. */
.chead {
  margin: 0 0 24px;
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card);
}
.chead__top { display: flex; align-items: center; gap: 14px; }
.chead__avatar {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 16px;
  object-fit: cover;
  color: #FFFFFF;
  font-size: 23.5px;
  font-weight: 700;
}
.chead__id { flex: 1; min-width: 0; }
.chead__name {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  color: var(--liquid-primary-label);
  white-space: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
.chead__name::-webkit-scrollbar { display: none; }
.chead__handle { margin: 2px 0 0; font-size: 13px; color: var(--liquid-secondary-label); }
.chead__addhandle {
  display: inline-block;
  margin-top: 2px;
  font-size: 13px;
  font-weight: 600;
  color: var(--liquid-channel-accent);
  text-decoration: none;
}
/* RETIRED 2026-10-07 — .chead__menu, the card's react menu; React is on the
 * capsule: .chead__menu { align-self: flex-start; } */
.chead__desc { margin: 14px 0 0; font-size: 15px; line-height: 1.45; color: rgba(0, 0, 0, 0.87); white-space: pre-line; }
.chead__line { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.chead__stats { flex: 1; display: flex; gap: 24px; min-width: 0; }
.chead__stat { display: flex; flex-direction: column; }
.chead__value { font-size: 17px; font-weight: 700; color: var(--liquid-primary-label); }
.chead__label { font-size: 12px; color: var(--liquid-secondary-label); }
.chead__notice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.4;
  font-weight: 600;
}
.chead__notice .nav__glyph { flex: 0 0 16px; width: 16px; height: 16px; margin-top: 1px; }
.chead__notice--caution { color: #8F6500; }
/* ── RETIRED 2026-10-07 — .chead__cost, .chead__reach, .chead__reachtitle,
 * .chead__reachsplit: the cost line (the Delivery row's chip now) and the
 * publisher's reach under the card, which the app dropped. They were:
 *   .chead__notice--caution, .chead__cost { color: #8F6500; }
 *   .chead__cost { margin: 0; font-size: 13px; font-weight: 600; }
 *   .chead__reach { margin-top: 18px; }
 *   .chead__reachtitle { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--liquid-secondary-label); }
 *   .chead__reachsplit { margin: 0; font-size: 15px; color: var(--liquid-primary-label); }
 * and the cost's dark #F4C542 in the two blocks below. */
.chead__fine { margin: 6px 0 0; font-size: 12px; line-height: 1.4; color: var(--liquid-secondary-label); }
.chead__notices { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; }
.chead__notices > p,
.chead__notices .chead__notice { margin: 0; }
p.chead__notices { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .chead__desc { color: rgba(255, 255, 255, 0.70); }
  :root:not([data-theme="light"]) .chead__notice--caution { color: #F4C542; }
}
:root[data-theme="dark"] .chead__desc { color: rgba(255, 255, 255, 0.70); }
:root[data-theme="dark"] .chead__notice--caution { color: #F4C542; }

/* The grouped rows. A group with a footer hands the gap to the footer. */
.chgroup--noted { margin-bottom: 0; }
.chgroup__note { margin: 0 0 24px; padding: 8px 14px 0; }

/* Mute's glyph is Palette.circle (notificationRowIconColor). */
.row__icon--circle { --tint: var(--liquid-circle-accent); }

/* A <button> row that opens a sheet takes the link rows' chevron. */
.row--disclose::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin: 0 4px 0 2px;
  border-top: 2px solid var(--liquid-tertiary-label);
  border-right: 2px solid var(--liquid-tertiary-label);
  border-radius: 1px;
  transform: rotate(45deg);
}
.row--button:hover { background: var(--liquid-fill-quiet); }

/* The sheets' extras: a row that is a link, the limit counter, its hint. */
a.ditem { color: inherit; text-decoration: none; }
a.ditem:hover { background: var(--liquid-fill-quiet); }
.ditem__glyph--ping { color: #5374CC; }
.dpicker__hint { margin: 0 0 12px; font-size: 12px; color: var(--liquid-secondary-label); text-align: center; }
.stepper { display: flex; align-items: center; justify-content: center; gap: 18px; padding: 12px 0 8px; }
.stepper__btn {
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: var(--liquid-fill-quiet);
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.stepper__value {
  width: 110px;
  border: 0;
  background: none;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 34px;
  font-weight: 700;
  text-align: center;
  -moz-appearance: textfield;
  appearance: textfield;
}
.stepper__value::-webkit-outer-spin-button,
.stepper__value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ditem__glyph--ping { color: #7C9CFF; }
}
:root[data-theme="dark"] .ditem__glyph--ping { color: #7C9CFF; }

/* ── THE CAPSULE — LiquidActionBar ────────────────────────────────────────────
 *
 * The verbs for the thing on screen, floating at the foot of the page: glyph
 * over an 11pt caption, in the chrome's own fill and the label colour — the
 * one grey bar, no domain tint. Sticky rather than fixed, so it rides at the
 * bottom of the window while the page scrolls and settles under the last
 * block at the end. On a phone it sits above the tab bar. */
.actbar {
  position: sticky;
  bottom: 16px;
  z-index: 5;
  display: flex;
  gap: 2px;
  max-width: 480px;
  margin: 24px auto 0;
  padding: 4px 6px;
  border-radius: var(--liquid-radius-capsule);
  background: var(--liquid-flat-chrome-fill);
  border: 1px solid var(--liquid-content-border);
  box-shadow: var(--liquid-chrome-shadow);
}
.actbar__form { display: contents; }
.actbar__item {
  flex: 1 1 0;
  min-width: 0;
  height: 52px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 0 6px;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: none;
  color: var(--liquid-primary-label);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}
.actbar__item:hover { background: var(--liquid-fill-quiet); }
.actbar__item .nav__glyph { width: 22px; height: 22px; }
.actbar__label { max-width: 100%; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ON A PHONE, LiquidPingBottomBar AT ITS OWN SIZES (2026-10-03, user asked).
 * It was the desktop's 480-wide strip of equal cells, which on a channel came
 * out wider and taller than the tab bar it sat on. The app's is 56 tall and
 * as wide as its items: each one its glyph (21) over a 10 semibold caption in
 * the secondary label, 16 either side — squeezed by 0.6, 0.45 and 0.34 at
 * four, five and six items — with a 24 hairline between neighbours. It sits
 * 16 above the safe area, because the tab bar is not drawn on these pages
 * (see the phone block); on a tab root it would clear the bar. */
@media (max-width: 700px) {
  .actbar {
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    width: max-content;
    max-width: calc(100vw - 32px);
    height: 56px;
    align-items: center;
    gap: 0;
    padding: 0 8px;
  }
  .app--tab .actbar { bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
  .actbar__item {
    position: relative;
    flex: 0 0 auto;
    height: 56px;
    padding: 0 16px;
    color: var(--liquid-secondary-label);
  }
  .actbar:has(> :nth-child(4)) .actbar__item { padding: 0 10px; }
  .actbar:has(> :nth-child(5)) .actbar__item { padding: 0 7px; }
  .actbar:has(> :nth-child(6)) .actbar__item { padding: 0 5px; }
  .actbar__item .nav__glyph { width: 21px; height: 21px; }
  .actbar__label { font-size: 10px; font-weight: 600; letter-spacing: -0.1px; }
  .actbar > * + .actbar__item::before,
  .actbar > * + .actbar__form > .actbar__item::before {
    content: "";
    position: absolute;
    left: 0;
    top: 16px;
    width: 1px;
    height: 24px;
    background: var(--liquid-separator);
  }
}
@media (hover: none) {
  .actbar__item:hover { background: none; }
}

/* ── THE CIRCLE EMBLEM — circle_emblem.dart (2026-09-23) ─────────────────────
 *
 * An SVG sized by its box: the band, the name and member count round it in
 * Outfit Bold, the picture or glyph inside. The circle's own colour comes in as
 * fill attributes; these are only for a circle with none (the domain pink,
 * which differs by theme) and for the muted disc on the rim. */
.emblem { position: relative; display: inline-block; width: 100%; aspect-ratio: 1; }
.emblem__svg { display: block; width: 100%; height: 100%; overflow: visible; }
.emblem__text { font-family: "Outfit", system-ui, sans-serif; font-weight: 700; }
.emblem__band--default { fill: var(--liquid-circle-accent); }
.emblem__centre--default { fill: color-mix(in srgb, var(--liquid-circle-accent) 18%, #FFFFFF); }
.emblem__glyph--default { stroke: var(--liquid-circle-accent); }
/* MUTED: THE WHOLE EMBLEM SAYS SO (2026-10-06, user) — the app's
 * CircleMutedVeil: blurred and dimmed under a centred bell-off. It was a 24px
 * coin on the rim (right: -2px; bottom: 2px; 24px; card surface; content
 * border; secondary label), which read as a badge count at a glance. */
.emblem__muted {
  position: absolute;
  inset: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.35);
  color: #fff;
  pointer-events: none;
}
.emblem__muted .nav__glyph { width: 36%; height: 36%; }
/* The blur is on the emblem itself, not a backdrop: the web has no glass
 * layer (TestTheGlassLayerStaysGone), and a plain filter on the picture is
 * the same look without one. */
.emblem:has(.emblem__muted) .emblem__svg { filter: blur(2px); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .emblem__muted { background: rgba(0, 0, 0, 0.45); }
}
:root[data-theme="dark"] .emblem__muted { background: rgba(0, 0, 0, 0.45); }

/* ── CIRCLES — circles_screen.dart ────────────────────────────────────────────
 *
 * The badge grid: the audience grid's metrics — width / 140 clamped to 3–5
 * columns, 10 between cells and 14 between rows, each emblem square to its
 * cell and capped at 132. No card behind a badge: a box round a circle is a
 * second edge saying what the first already says. */
.cgrid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px 10px;
  margin: 0 0 24px;
}
@media (min-width: 560px) { .cgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* Five from 1200, where the column widens to 760 (THE WIDE SCREEN, at the foot
 * of this file); the 700 rule beside the 560 one repeated it (2026-10-04). */
@media (min-width: 1200px) { .cgrid { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.cbadge { position: relative; display: flex; justify-content: center; }
.cbadge__link { display: block; width: 100%; max-width: 132px; min-width: 56px; }
.cbadge__menu { position: absolute; top: 0; right: 0; }

/* An invitation: the one thing here waiting on the reader, so the one card
 * with the emerald rim (kEmeraldNormal at 50%). Name 15/700, "Invited by" 12,
 * then Accept and Decline half and half. */
.invcards { display: flex; flex-direction: column; gap: 10px; margin: 0 0 24px; }
.invcard {
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid rgba(12, 136, 99, 0.5);
  border-radius: var(--liquid-radius-card);
}
.invcard--page { margin: 0 0 24px; }
.invcard__name { margin: 0; font-size: 15px; font-weight: 700; }
.invcard__by { margin: 2px 0 0; font-size: 12px; color: var(--liquid-secondary-label); }
.invcard__btns { display: flex; gap: 10px; margin-top: 12px; }
.invcard__btns > .btn { flex: 1 1 0; }

/* ── CIRCLE DETAIL — _CircleHero and _MemberRow ──────────────────────────────
 *
 * The emblem centred at 46% of the column, 96–148; the description centred
 * under it, 15/1.45 in the secondary ink, four lines at most. */
.chero { display: flex; flex-direction: column; align-items: center; margin: 0 0 24px; padding: 0 24px; }
.chero__emblem { width: 46%; min-width: 96px; max-width: 148px; }
.chero__desc {
  margin: 24px 0 0;
  max-width: 480px;
  font-size: 15px;
  line-height: 1.45;
  color: var(--liquid-secondary-label);
  text-align: center;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mrow__avatar {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  color: #FFFFFF;
  font-size: 15px;
  font-weight: 700;
}
.mrow--gone { opacity: 0.5; }
.mrow__sub { white-space: normal; }
.mrow__remove { display: contents; }
.mrow__removebtn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -4px -8px -4px 0;
  border: 0;
  background: none;
  color: var(--liquid-destructive-icon);
  cursor: pointer;
}
.mrow__removebtn .nav__glyph { width: 22px; height: 22px; }
.row--empty { margin: 0; font-size: 15px; color: var(--liquid-secondary-label); }

/* The second question of a delete: a card with the count and both answers. */
.confirmcard {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin: 0 0 24px;
  padding: 24px 20px 20px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card);
  text-align: center;
}
.confirmcard__glyph { color: #8F6500; }
.confirmcard__glyph .nav__glyph { width: 32px; height: 32px; }
.confirmcard__body { margin: 0; max-width: 440px; font-size: 15px; line-height: 1.45; }
.confirmcard__btns { display: flex; gap: 10px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .confirmcard__glyph { color: #F4C542; }
}
:root[data-theme="dark"] .confirmcard__glyph { color: #F4C542; }

/* A ROW INSIDE A FORM (a destructive row like Leave or Unfollow) keeps the
 * hairline to the row above it: .row__form is display: contents, which leaves
 * the button row a child of the form rather than the sibling .row + .row needs. */
.row + .row__form > .row::before {
  content: "";
  position: absolute;
  top: 0;
  left: 56px;
  right: 0;
  height: 1px;
  background: var(--liquid-separator);
}

/* The strip's emblem is the SVG ring now: no clip, no fill of its own. */
.circlestrip__emblem--ring { border-radius: 0; overflow: visible; }
.circlestrip__emblem--ring .emblem__svg { width: 100%; height: 100%; }
/* The veil shows on the strip too since 2026-10-06 (user: "make the circle
 * is muted to be seen easily"); it was display: none here. */

/* ── PLACE REQUEST APPROVAL — place_request_approval_screen.dart (2026-09-23) ──
 * A 22pt heading, two 13pt sentences (the second muted), then sections whose
 * cards hold plain lines: a 17pt glyph in placeRequestIconColor beside 15pt
 * text — lighter than a row, because nothing in them is pressable. Accept is
 * the emerald button at the foot, Decline the quiet one under it. */
.prhead { padding: 0 4px 8px; }
.prhead__title { margin: 0 0 12px; font-size: 22px; font-weight: 700; line-height: 1.25; }
.prhead__p { margin: 0 0 10px; font-size: 13px; line-height: 1.4; }
.prhead__p--muted { color: var(--liquid-secondary-label); }
.prcard { padding: 14px 16px 6px; }
.prline {
  --tint: #5374CC;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0;
  padding-bottom: 10px;
  font-size: 15px;
  line-height: 1.4;
  color: var(--liquid-primary-label);
}
.prline > .nav__glyph { flex: 0 0 auto; width: 17px; height: 17px; margin-top: 2px; color: var(--tint); }
.prline > span { min-width: 0; overflow-wrap: anywhere; }
.prline--brand   { --tint: #0C8863; }
.prline--channel { --tint: var(--liquid-channel-accent); }
.prline--premium { --tint: #6D4BD8; }
.prline--caution { --tint: #9E6F00; }
.prline--ping   { --tint: #5374CC; }
/* A term asked for that the reader's own plan leaves out of the ping. */
.prline--struck > span { text-decoration: line-through; color: var(--liquid-tertiary-label); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .prline--premium { --tint: #A993FF; }
}
:root[data-theme="dark"] .prline--premium { --tint: #A993FF; }
.prverbs { display: flex; flex-direction: column; gap: 8px; margin: 8px 0 24px; }
.prverbs > .btn { width: 100%; }

/* ── THE PROFILE — profile_screen.dart (2026-09-24) ─────────────────────────
 * The hero is the person page's (.phero); these are the lines the profile adds
 * under the handle: the email, and the plan chip (PlanBadge). */
.phero__email {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--liquid-secondary-label);
  min-width: 0;
}
.phero__email .nav__glyph { width: 13px; height: 13px; flex: 0 0 auto; color: var(--liquid-secondary-label); }
.phero__email span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phero__chips { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 10px 0 0; }

/* PlanBadge: an outlined capsule in the tier's colour, PlanBadge.colorFor —
 * the same Mid values in both themes, as the app draws them. */
.planchip {
  --tint: #80766C;
  display: inline-block;
  padding: 3px 10px;
  border-radius: 20px;
  border: 1px solid color-mix(in srgb, var(--tint) 55%, transparent);
  color: var(--tint);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
}
.planchip--o1 { --tint: #7F61E4; }
.planchip--o2 { --tint: #7F61E4; }
.planchip--o3 { --tint: #7F61E4; }

/* The Current Plan row's crown takes the tier's colour; Credits takes the
 * paid-tag colour (Palette.credit). */
.row__icon--tier-1 { --tint: #80766C; }
.row__icon--tier0  { --tint: #80766C; }
.row__icon--tier1  { --tint: #7F61E4; }
.row__icon--tier2  { --tint: #7F61E4; }
.row__icon--tier3  { --tint: #7F61E4; }
.row__icon--credit { --tint: #8F6500; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .row__icon--credit { --tint: #F4C542; }
}
:root[data-theme="dark"] .row__icon--credit { --tint: #F4C542; }

/* RETIRED 2026-09-28 — .switch--info: the find-me switch was on in
 * Palette.infoMid. A switch is the brand, as every other one here. */

/* ── EDIT PROFILE — edit_profile_screen.dart ──────────────────────────────── */
.pedit__photo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin: 8px 0 24px;
}
/* Change and Remove under the photo, side by side, as _PhotoPicker. */
.pedit__photoacts { display: flex; justify-content: center; gap: 8px; }
.pedit__photoacts > .btn { cursor: pointer; }
.pedit__remove { color: var(--liquid-destructive-icon); }
.pedit__photo > .fielderror { margin: 0; text-align: center; }
.pedit__photo > .fielderror[hidden] { display: none; }
/* The keyboard focus of the hidden file field shows on its label. */
.pedit__photoacts > label:has(+ #photo:focus-visible) { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
.phero__photo--edit { width: 104px; height: 104px; font-size: 40px; }
.pedit { display: flex; flex-direction: column; margin: 0 0 24px; }
.pedit__label {
  margin: 0 4px 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--liquid-secondary-label);
}
.pedit > .input,
.pedit > .input--prefixed { margin: 0 0 24px; }
.pedit > .fielderror { margin: -16px 4px 24px; }
.fielderror { font-size: 13px; color: var(--liquid-destructive-icon); }
.pedit__foot { margin: 8px 0 0; }
.pedit__foot > .btn { width: 100%; }

/* ── PROFILE NOTICE — the answer to a reset, or a refused confirm ──────────
 * One line over the page. Red by default, because refusal is what it says
 * most; --ok is the reset mail sent. The same pair as .authform__status. */
.pnotice {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-radius: var(--liquid-radius-row);
  font-size: 14px;
  background: var(--liquid-destructive-surface);
  color: var(--liquid-destructive-label);
}
.pnotice--ok {
  background: var(--liquid-fill-quiet);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-primary-label);
}

/* ── CONFIRM IT'S YOU — Export My Data and Delete Account ──────────────────
 * The app's dialog text in a card with the act's mark, then the sign-in panel.
 * Export is emerald (kEmeraldNormal, the row's tint); delete is the
 * destructive red, on the mark and the button alike. */
.stepup__head {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  margin: 0 0 16px;
  padding: 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
  --tint: #0C8863;
}
.stepup__head--delete { --tint: var(--liquid-destructive-icon); }
.stepup__mark {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--tint) 18%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--tint) 40%, transparent);
  color: var(--tint);
}
.stepup__mark .nav__glyph { width: 24px; height: 24px; }
/* deleteAccountMessage carries its own blank line — pre-line keeps it. */
.stepup__msg { margin: 0; align-self: center; font-size: 15px; line-height: 1.4; white-space: pre-line; }

.stepup {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0 0 16px;
  padding: 20px 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
}
.stepup__title { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.2px; }
.stepup__sub { margin: -6px 0 4px; font-size: 13px; line-height: 1.35; color: var(--liquid-secondary-label); }
.stepup__pw { display: flex; flex-direction: column; }
.stepup__pw > .input { margin: 0 0 16px; }
.stepup__pw > .input[readonly] { color: var(--liquid-secondary-label); }
.stepup__go { min-height: 48px; font-weight: 600; }
.stepup__status {
  margin: 0;
  padding: 10px 12px;
  border-radius: var(--liquid-radius-row);
  font-size: 13px;
  background: var(--liquid-destructive-surface);
  color: var(--liquid-destructive-label);
}
.stepup__status--ok {
  background: var(--liquid-fill-quiet);
  border: 1px solid var(--liquid-content-border);
  color: var(--liquid-primary-label);
}
.stepup__status[hidden] { display: none; }
.stepup__cancel { margin: 0 0 24px; text-align: center; font-size: 15px; }

/* Delete Account's footer is the section's own (LiquidListSection footer:),
 * so it hangs off the group rather than floating a section-gap below it. */
.pdelete__note { margin: -18px 0 24px; padding: 0 14px; }

/* ── SETTINGS TINTS — RETIRED 2026-09-28 ─────────────────────────────────────
 * .row__icon--email, --rose, --violet, --teal, --blue, --indigo: the
 * per-row hues of settings_screen.dart's tint(light, dark), each of which was
 * some meaning's colour. */

/* ── SETTINGS TILES (2026-09-28) — LiquidListTile.iconFill ───────────────────
 * The iOS tile: white glyph on a solid fill of --tint. A filled square reads
 * as a label, not a meaning, so the fills are Palette.tile* — hues no meaning
 * owns — one per group, never the same as the group beside it. Meaning rows
 * (notifications, places) fill with their meaning: row__icon--ping. Same in
 * both themes, as the app's are. */
.row__icon--fill { color: #FFFFFF; background: var(--tint); }
.row__icon--slate    { --tint: #5B6B80; }
.row__icon--olive    { --tint: #6B7A2E; }
.row__icon--graphite { --tint: #57504A; }
.row__icon--stone    { --tint: #80766C; }
/* A fill needs the mid red: the dark theme's destructive icon is a pale red a
 * white glyph vanishes on. */
.row__icon--fill.row__icon--urgent { --tint: #D44254; }

/* A row's trailing control — the Permissions row's tick or Allow pill. */
.row > .row__trailing { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.permok { display: inline-flex; color: #0C8863; }
.permok .nav__glyph { width: 20px; height: 20px; }
.permok[hidden], .permallow[hidden] { display: none; }
/* LiquidPillButton at LiquidSize.compact, flat on the row, 12pt label. */
.permallow { min-height: 28px; padding: 0 14px; font-size: 12px; }

/* ── MY DEVICES / MY EMAILS — a card per item (2026-09-24) ────────────────
 * DeliveryMethodCard: the content surface at the card radius, 16 inset, a
 * 48pt rounded-square tile in the method's colour at 10%/15% with a 1.5
 * ring, a 16 semibold title, the 13 summary, and a foot row under the text
 * (16 + 48 + 14 = 78 in) with the badge first and the footnote hard right. */
/* ── RETIRED — .dmlist (2026-10-08) ── the column of cards; Devices & Emails is
 * grouped rows. Was: .dmlist { display: flex; flex-direction: column; gap: 12px;
 * margin: 0 0 24px; } */
.dmcard {
  position: relative;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
}
.dmcard:has(.dmcard__link:hover) { background: var(--liquid-fill-quiet); }
.dmcard--inert { opacity: 0.75; }
/* The link covers the card; the trash and resend sit above it. */
.dmcard__link { position: absolute; inset: 0; border-radius: inherit; z-index: 0; }
.dmcard__link:focus-visible { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
.dmcard__act { position: relative; z-index: 1; margin: 0; }
.dmcard__top { display: flex; align-items: flex-start; gap: 14px; padding: 16px 10px 16px 16px; }
.dmcard:has(.dmcard__foot) .dmcard__top { padding-bottom: 10px; }
.dmcard__tile {
  --tint: var(--liquid-secondary-label);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 14px;
  color: var(--tint);
  background: color-mix(in srgb, var(--tint) 10%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--tint) 40%, transparent);
}
.dmcard__tile .nav__glyph { width: 22px; height: 22px; }
/* No per-method tint (2026-09-28): a delivery method's glyph is ink2 in both
 * themes, deliveryMethodGlyphColor in the app. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dmcard__tile { background: color-mix(in srgb, var(--tint) 15%, transparent); border-color: color-mix(in srgb, var(--tint) 55%, transparent); }
}
:root[data-theme="dark"] .dmcard__tile { background: color-mix(in srgb, var(--tint) 15%, transparent); border-color: color-mix(in srgb, var(--tint) 55%, transparent); }
.dmcard__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
/* An address is never cut: the end is what tells two apart. */
.dmcard__title { font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
.dmcard__summary { font-size: 13px; line-height: 1.3; color: var(--liquid-secondary-label); }
.dmcard__trash {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--liquid-destructive-icon);
  cursor: pointer;
}
.dmcard__trash:hover { background: var(--liquid-fill-quiet); }
.dmcard__trash .nav__glyph { width: 18px; height: 18px; }
.dmcard__trash--refused { color: var(--liquid-tertiary-label); }
.dmcard__foot { display: flex; align-items: center; gap: 12px; padding: 0 16px 14px 78px; }
.dmcard__detail { flex: 1; min-width: 0; text-align: end; font-size: 11.5px; color: var(--liquid-tertiary-label); }
.dmcard__resend {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--liquid-primary-action);
  cursor: pointer;
}
/* DeliveryMethodBadge: a small tinted capsule. */
.dmbadge {
  --tint: var(--liquid-secondary-label);
  display: inline-flex;
  align-items: center;
  min-height: 22px;
  padding: 0 9px;
  border-radius: var(--liquid-radius-capsule);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--tint);
  background: color-mix(in srgb, var(--tint) 14%, transparent);
}
.dmbadge--ping   { --tint: #5374CC; }
.dmbadge--caution { --tint: #9E6F00; }

/* ── RETIRED — .scope, .scope__*, .scopetile, .scopetile* (2026-10-08) ────────
 * THE FOUR SWITCHES as a page: a centred title, four square tiles in each
 * source's colour, the cost lines and one Done. Every source chooses its own
 * devices and address on its own page now (129), and "Receive my pings" is
 * a switch on the Devices & Emails row. The rules are in git history. */

/* ── ADD EMAIL — the sheet as a page ───────────────────────────────────── */
.emailadd {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0 auto 24px;
  max-width: 520px;
  padding: 20px 16px;
  background: var(--liquid-content-surface);
  border: 1px solid var(--liquid-content-border);
  border-radius: var(--liquid-radius-card, 20px);
}
.emailadd__title { margin: 0; font-size: 18px; font-weight: 700; }
.emailadd__desc { margin: 0 0 12px; font-size: 12px; line-height: 1.4; color: var(--liquid-secondary-label); }
.emailadd__submit { width: 100%; }
/* RETIRED 2026-10-08 with .scopetile — its phone width:
 * @media (max-width: 480px) { .scopetile { width: 72px; height: 78px; border-radius: 14px; } } */

/* ── EDIT PING — the form in edit mode (2026-09-24) ─────────────────────────
 * The composer's parts, a To row above them and the dock below. The fieldset
 * only exists to disable a finished ping's fields in one attribute. */
.pingedit__fields { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; border: 0; min-width: 0; }
.pingedit__fields:disabled { opacity: 0.6; }
.editto { margin: 0; }
.editto__change {
  flex: 0 0 auto;
  font-size: 15px;
  font-weight: 600;
  color: var(--flow-accent);
}
/* ── RETIRED — .pingedit__media (2026-09-24) ───────────────────────────────
 * The edit form's read-only box for "the photographs or voice note, kept by a
 * Save and changed in the app". The web has the Attach section now
 * (.attach__*), so the edit form changes them itself. */
.editdock__summary {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--liquid-primary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.editdock__summary [data-edit-state="none"] { color: var(--liquid-tertiary-label); }
.editdock__summary [hidden] { display: none; }
/* "Fix the problem above to save" (2026-10-13) in the caution amber, as the
 * app's dock tints it (Palette.caution). */
.editdock__summary .editdock__fix { color: #8F6500; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .editdock__summary .editdock__fix { color: #F4C542; }
}
:root[data-theme="dark"] .editdock__summary .editdock__fix { color: #F4C542; }

/* THE SOCIAL WATCH CARD (2026-09-24) — _SocialWatchCard under a received
 * YouTube or Twitch firing: the thumbnail at 16:9 with a white play disc and
 * the provider named at the foot over a dark fade, or, with no picture, a
 * plain button. White on a photograph is the one place a fixed colour is
 * right: the ground is the picture, not the theme. */
.watchcard {
  position: relative;
  display: block;
  margin: 16px 0 24px;
  aspect-ratio: 16 / 9;
  border-radius: var(--liquid-radius-card);
  overflow: hidden;
  background: var(--liquid-fill-quiet);
}
.watchcard::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgb(0 0 0 / 0.02), rgb(0 0 0 / 0.45));
}
.watchcard__img { display: block; width: 100%; height: 100%; object-fit: cover; }
.watchcard__play {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: 56px;
  height: 56px;
  margin: -28px 0 0 -28px;
  border-radius: 50%;
  background: #fff;
}
.watchcard__play::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -10px 0 0 -6px;
  border-style: solid;
  border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent rgb(0 0 0 / 0.87);
}
.watchcard__label {
  position: absolute;
  left: 12px;
  bottom: 12px;
  z-index: 1;
  padding: 6px 12px;
  border-radius: var(--liquid-radius-capsule);
  background: rgb(0 0 0 / 0.55);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
}
.watchbtn { display: inline-flex; margin: 16px 0 24px; }

/* A SELECT ON A SETTINGS ROW (2026-09-24) — Theme, the app's option sheet as
 * the browser's own picker: trailing, content-sized, and never wider than the
 * row can give it beside the title. */
.row__select { flex: 0 1 auto; width: auto; max-width: 55%; margin-left: auto; }

/* ── EDITOR COLOUR ROW AND PICTURE WELL — channel and circle editors ───────
 * (2026-09-24) partial-editor-picture.html. A swatch's colour is a CLASS —
 * card__avatar--tN for a channel, tint--cN for a circle — never a style
 * attribute: style-src has no 'unsafe-inline'. The ring is outside the swatch,
 * so the colour is judged at full size, as in the app. A channel still on its
 * derived colour wears the ring on that slot until another is checked. */
.swatches { border: 0; margin: 0 0 24px; padding: 0; min-width: 0; }
.swatches__row { display: flex; flex-wrap: wrap; gap: 10px 12px; }
.swatch {
  position: relative;
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  cursor: pointer;
  background: var(--tint);
}
.swatches--round .swatch { border-radius: 50%; }
.swatch:has(input:checked),
.swatches__row:not(:has(input:checked)) .swatch--derived {
  outline: 2px solid var(--liquid-primary-label);
  outline-offset: 2px;
}
.swatch:has(input:focus-visible) { outline: 2px solid var(--liquid-accent); outline-offset: 2px; }
/* A custom colour from the app's picker sheet, which this site cannot paint. */
.swatch--custom { background: conic-gradient(#EF4444, #F59E0B, #10B981, #3B82F6, #8B5CF6, #EF4444); }
.swatches__footer { margin: 8px 0 0; font-size: 12px; line-height: 1.4; color: var(--liquid-secondary-label); }

/* A circle's eight (kCircleColourChoices), drawn as written in both themes as
 * the app draws them; no colour is the circles domain pink. */
.tint--c0 { --tint: #3B82F6; }
.tint--c1 { --tint: #8B5CF6; }
.tint--c2 { --tint: #EC4899; }
.tint--c3 { --tint: #EF4444; }
.tint--c4 { --tint: #F59E0B; }
.tint--c5 { --tint: #10B981; }
.tint--c6 { --tint: #14B8A6; }
.tint--c7 { --tint: #64748B; }
.tint--cdefault { --tint: var(--liquid-circle-accent); }
.phero__photo[class*="tint--c"] { background: var(--tint); }
.phero__photo--round { border-radius: 50%; }

/* ── THE HOLD TIP — reaction_hold_tip.dart (2026-09-24) ─────────────────────
 * "Press and hold to react", once per device, centred on the first reactable
 * card on screen; the layout's hold script places it. Inverse ink on a
 * capsule, so it reads as a note laid ON the card rather than its content. */
.holdtip-host { position: relative; }
.holdtip {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100% - 24px);
  padding: 8px 10px 8px 12px;
  border-radius: 999px;
  background: var(--liquid-primary-label);
  color: var(--liquid-content-surface);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  animation: holdtip-in 260ms ease-out;
}
.holdtip__text { overflow: hidden; text-overflow: ellipsis; }
.holdtip__x { display: inline-flex; opacity: 0.7; }
.holdtip__x svg { width: 14px; height: 14px; }
@keyframes holdtip-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.94); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@media (prefers-reduced-motion: reduce) { .holdtip { animation: none; } }

/* ── HTMX FAULTS — 2026-09-24 ────────────────────────────────────────────────
 * The region every failed hx- request writes into (layout.html, "HTMX
 * FAILURES"; lib-render.go, writeFaultFragment). Pinned to the foot of the
 * window rather than placed in the column, because the control that failed can
 * be anywhere — a sheet, a card menu, the bottom of a long list — and the
 * message has to be seen from wherever the person is looking.
 *
 * Empty, it takes no space and catches no clicks: pointer-events pass through
 * the region and are only turned back on for the message itself.
 *
 * The red is .formerror's red. --liquid-destructive-surface is translucent in
 * the dark theme, which is right on a card and wrong over scrolling content, so
 * it is laid over the solid surface rather than used alone. */
.fault-region {
  position: fixed;
  left: 50%;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 70;
  transform: translateX(-50%);
  width: min(560px, calc(100% - 32px));
  pointer-events: none;
}
.fault-region:empty { display: none; }
/* Under 700px the rail is a bar along the foot of the window, so the message
 * sits where .nav__menu opens: just above it rather than over the Create button. */
@media (max-width: 700px) {
  .fault-region { bottom: calc(92px + env(safe-area-inset-bottom, 0px)); }
}
.fault {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 8px 10px 14px;
  border-radius: var(--liquid-radius-row);
  background:
    linear-gradient(var(--liquid-destructive-surface), var(--liquid-destructive-surface)),
    var(--liquid-grouped-surface);
  color: var(--liquid-destructive-label);
  border: var(--liquid-hairline) solid var(--liquid-destructive-rim);
  box-shadow: var(--liquid-chrome-shadow);
  font-size: 14px;
  line-height: 1.4;
  animation: fault-in 200ms ease-out;
}
.fault__text { flex: 1 1 auto; min-width: 0; }
.fault__title { margin: 0; font-weight: 600; }
.fault__body { margin: 2px 0 0; }
/* 32px drawn, 44px pressed: the padding stays inside the message's own box. */
.fault__close {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin: -4px 0;
  padding: 0;
  border: 0;
  border-radius: var(--liquid-radius-capsule);
  background: none;
  color: inherit;
  font: inherit;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.fault__close::after { content: ""; position: absolute; inset: -6px; }
.fault__close:hover { background: var(--liquid-fill-quiet); }
@keyframes fault-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .fault { animation: none; } }

/* ── A LOCATION BLOCK (2026-09-25) ──────────────────────────────────────────
 *
 * The app draws a live map; a page draws the pin, the name, the address, how
 * the point was obtained ("Shared from their phone" / "Picked on the map"),
 * and an "Open in Maps" line. The whole card is the link. See
 * partial-ping-blocks.html.
 */
.blocks .locationcard {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  text-decoration: none;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: 12px;
  padding: 12px 14px;
  margin: 0 0 16px;
  color: inherit;
}
.locationcard__pin {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 11px;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--liquid-ping-accent) 14%, transparent);
  color: var(--liquid-ping-accent);
}
.locationcard__pin .nav__glyph { width: 20px; height: 20px; }
.locationcard__text { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.blocks .locationcard .title { color: var(--liquid-primary-label); font-weight: 600; overflow-wrap: anywhere; }
.blocks .locationcard .site { color: var(--liquid-secondary-label); font-size: 13px; }
.locationcard__source { color: var(--liquid-tertiary-label); font-size: 12px; }
.locationcard__open { color: var(--liquid-accent); font-size: 13px; font-weight: 600; margin-top: 4px; }

/* ── RETIRED 2026-10-05 — .attach__place, .attach__placeglyph, .attach__placelabel, .attach__placecoords ──
 * The composer's ONE place row (📍, the name, the coordinates, ✕). Places
 * are a run of .attachrow rows now (.attachlist--compose), as any attachment
 * kind is. Was the voice row's shape, one line. */

/* ── GETTING STARTED (2026-09-25) ───────────────────────────────────────────
 *
 * The new sign-up's card at the top of the Inbox, the app's GettingStartedCard:
 * a title and a count, a segmented bar (one segment per step — style-src
 * forbids a computed width), and the steps as rows that tick themselves.
 */
.gscard {
  margin: 0 0 var(--space-section);
  padding: 18px 18px 8px;
  border-radius: var(--liquid-radius-card);
  background:
    radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--liquid-accent) 14%, transparent), transparent 60%),
    var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-content-border);
  animation: gscard-in 0.36s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes gscard-in {
  from { opacity: 0; transform: translateY(8px) scale(0.985); }
  to { opacity: 1; transform: none; }
}
.gscard__head { display: flex; align-items: flex-start; gap: 12px; }
.gscard__titles { flex: 1 1 auto; min-width: 0; }
.gscard__title { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.2px; color: var(--liquid-primary-label); }
.gscard__progress { margin: 4px 0 0; font-size: 14px; color: var(--liquid-secondary-label); }
/* A PLAIN ✕ IN THE TOP-RIGHT CORNER (2026-09-26), as the app draws it: no
 * grey disc, and pulled up and out so the glyph sits in the corner rather than
 * floating at the height of the title. The hit area stays 32px. */
.gscard__close {
  width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  margin: -6px -8px 0 0;
  border: 0; cursor: pointer;
  background: transparent;
  color: var(--liquid-secondary-label);
}
.gscard__close:hover { color: var(--liquid-primary-label); }
.gscard__close .nav__glyph { width: 16px; height: 16px; }
.gscard__bar { display: flex; gap: 6px; margin: 14px 0 6px; }
.gscard__seg { flex: 1 1 0; height: 6px; border-radius: 3px; background: var(--liquid-fill-quiet); transition: background 0.3s ease; }
.gscard__seg--done { background: var(--liquid-accent); }
.gscard__steps { list-style: none; margin: 0; padding: 0; }
.gsstep {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0;
  text-decoration: none; color: inherit;
  border-top: var(--liquid-hairline) solid var(--liquid-separator);
}
.gscard__steps li:first-child .gsstep { border-top: 0; }
.gsstep__icon {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: 11px;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--liquid-accent) 12%, transparent);
  color: var(--liquid-accent);
}
.gsstep__icon .nav__glyph { width: 18px; height: 18px; }
/* DONE IS A PLAIN GREEN CIRCLE WITH A TICK (2026-09-26), not the squircle an
 * open step's glyph sits in — the same shape filled read as another step. */
.gsstep--done .gsstep__icon {
  width: 28px; height: 28px; margin: 4px;
  border-radius: 50%;
  background: var(--liquid-accent); color: #fff;
}
.gsstep--done .gsstep__icon .nav__glyph { width: 15px; height: 15px; }
.gsstep__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gsstep__title { font-size: 16px; font-weight: 600; color: var(--liquid-primary-label); }
.gsstep--done .gsstep__title { color: var(--liquid-secondary-label); text-decoration: line-through; text-decoration-thickness: 1px; }
.gsstep__body { font-size: 13px; color: var(--liquid-secondary-label); }
.gsstep__chev { color: var(--liquid-tertiary-label); }
.gsstep__chev .nav__glyph { width: 16px; height: 16px; }
.gscard--complete .gscard__title { color: var(--liquid-accent); }
@media (prefers-reduced-motion: reduce) { .gscard { animation: none; } }

/* ══ "PING SENT" — THE DONE PAGE (2026-09-26) ════════════════════════════════
 * ping_creation_ad_screen.dart's success state: the ping-blue disc pops in
 * (420ms, elastic), the tick scales up inside it (380ms), then the title and
 * the sentence rise and fade in (520ms). The hero is centred in the space
 * above the foot, where Done — and, for a free account past its grace, the ad
 * — sit. No page head: the app's screen has no bar, and the title here is the
 * heading. */
.page:has(.pingdone) > .pagehead { display: none; }
.pingdone {
  --flow-accent: var(--liquid-ping-accent);
  display: flex;
  flex-direction: column;
  min-height: calc(100dvh - 140px);
  max-width: 560px;
  margin: 0 auto;
}
.pingdone__hero {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  padding: 24px 0;
  text-align: center;
}
.pingdone__disc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--flow-accent);
  color: #fff;
  box-shadow: 0 8px 24px -2px color-mix(in srgb, var(--flow-accent) 35%, transparent);
  animation: pingdone-pop 420ms cubic-bezier(.34, 1.56, .64, 1) both;
}
.pingdone__tick { display: inline-flex; animation: pingdone-tick 380ms cubic-bezier(.33, 1, .68, 1) 420ms both; }
.pingdone__tick .nav__glyph { width: 52px; height: 52px; stroke-width: 2.6; }
.pingdone__words { animation: pingdone-rise 520ms cubic-bezier(.33, 1, .68, 1) 800ms both; }
.pingdone__title {
  margin: 0;
  font-size: 28px;
  font-weight: 500;
  letter-spacing: -0.4px;
  line-height: 1.2;
  color: var(--liquid-primary-label);
}
.pingdone__body {
  margin: 10px 0 0;
  padding: 0 12px;
  font-size: 15px;
  line-height: 1.6;
  letter-spacing: 0.1px;
  white-space: pre-line;
  color: var(--liquid-secondary-label);
}
.pingdone__foot { display: flex; flex-direction: column; gap: 24px; padding-bottom: 20px; }
/* The doubled gap between a paid rectangle and the button everybody on this
 * screen is reaching for — the app's safety margin, not rhythm. */
.pingdone__foot--ad { gap: 48px; }
/* LiquidPrimaryCapsule in the ping blue. */
.pingdone__done {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 52px;
  border-radius: 999px;
  background: var(--flow-accent);
  color: var(--liquid-on-ping-accent);
  font-size: 17px;
  font-weight: 600;
  text-decoration: none;
  transition: transform 120ms ease;
}
.pingdone__done:active { transform: scale(0.97); }
.pingdone__done:focus-visible { outline: 2px solid var(--flow-accent); outline-offset: 3px; }
@keyframes pingdone-pop { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes pingdone-tick { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes pingdone-rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .pingdone__disc, .pingdone__tick, .pingdone__words { animation: none; }
}

/* ── THE BOTTOM ACTION BAR — 2026-09-29 ─────────────────────────────────────
 * The app's ping view carries ONE Edit on its bottom capsule instead of an
 * Edit link on every section that opened the same form. A window 700px wide
 * or less is that screen, so it gets the same capsule (page-ping-detail.html,
 * .actionbar) and the duplicate links step aside (.group__action--wide). A
 * wider window keeps the links: it has no bottom bar to put Edit on.
 *
 * GREY, like the app's — the one capsule that carries no accent. It floats
 * where .nav__menu and the fault region already float, just above the rail
 * that is a bar along the foot at this width, and UNDER the rail's own layer
 * (z 20) so the Create menu opens over it rather than behind it. */
.actionbar { display: none; }
@media (max-width: 700px) {
  .group__action--wide { display: none; }
  /* THE APP'S SIZES, AND ON ITS OWN AT THE FOOT (2026-10-03): 56 tall, the
   * glyph 21 over a 10 semibold caption in the secondary label, 16 above the
   * safe area — the tab bar is not drawn on a pushed page (see the phone
   * block), so nothing sits under it. On a tab root it would clear the bar. */
  .actionbar {
    display: flex;
    position: fixed;
    left: 50%;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    z-index: 15;
    transform: translateX(-50%);
    align-items: center;
    height: 56px;
    padding: 0 8px;
    border: var(--liquid-hairline) solid var(--liquid-separator);
    border-radius: var(--liquid-radius-capsule);
    background: var(--liquid-flat-chrome-fill);
    box-shadow: var(--liquid-chrome-shadow);
  }
  .app--tab .actionbar { bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
  .actionbar__item {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    height: 56px;
    padding: 0 16px;
    border-radius: var(--liquid-radius-capsule);
    color: var(--liquid-secondary-label);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: -0.1px;
    text-decoration: none;
  }
  .actionbar__item:hover,
  .actionbar__item:focus-visible { background: var(--liquid-fill-quiet); }
  .actionbar__item svg { width: 21px; height: 21px; }
  /* Room under the last section for the capsule: its 56, its 16, and air. */
  .app:not(.app--tab) .shell .page:has(.actionbar) {
    padding-bottom: calc(16px + 56px + 24px + env(safe-area-inset-bottom, 0px));
  }
  .app--tab .shell .page:has(.actionbar) {
    padding-bottom: calc(96px + 72px + env(safe-area-inset-bottom, 0px));
  }
}
@media (hover: none) {
  .actionbar__item:hover { background: none; }
}

/* ── THE REPEAT PANEL'S OPTIONS (2026-10-02) ────────────────────────────────
 *
 * The app's When sheet, in the Repeat panel: every N, the weekly shortcuts,
 * the month's dates or weekday, a yearly day and month, once / several /
 * every few hours, the start and the end, and the preview of the first
 * sends. See partial-ping-form.html and lib-repeat.go.
 *
 * [hidden] WINS INSIDE THE PANEL. Its rows are display:flex, which outranks
 * the browser's own [hidden] rule, so a row the script hid stayed drawn. */
.whenpanel [hidden] { display: none !important; }
.whenpanel__group { display: flex; flex-direction: column; gap: 10px; }
.whenpanel__pills { display: flex; flex-wrap: wrap; gap: 6px; }
.whenpanel__pair { display: flex; gap: 8px; }
.whenpanel__pair .input { max-width: 150px; }
.whenpanel__times { display: flex; flex-wrap: wrap; gap: 8px; }
.whenpanel__times .input { max-width: 140px; }
.input--count { max-width: 110px; }

/* A pill: a radio or a button in a capsule, chosen in the flow's accent —
 * the whenchip's selected state at the sheet's smaller size. */
.whenpill {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 12px;
  border-radius: var(--liquid-radius-capsule);
  border: 1px solid var(--liquid-separator);
  background: transparent;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: var(--type-caption);
  font-weight: 600;
  cursor: pointer;
}
.whenpill input { position: absolute; opacity: 0; pointer-events: none; }
.whenpill:has(input:checked) {
  background: color-mix(in srgb, var(--flow-accent) 12%, var(--liquid-content-surface));
  border-color: color-mix(in srgb, var(--flow-accent) 85%, transparent);
  color: var(--flow-accent);
}
.whenpill:focus-visible,
.whenpill:has(input:focus-visible) { outline: 2px solid var(--flow-accent); outline-offset: 2px; }
.whenpill .lockmark .nav__glyph { width: 13px; height: 13px; }

/* The month's 31 days, seven to a row like a calendar, and the wider boxes
 * for "Last day" and the week words. */
.daybar--month { display: grid; grid-template-columns: repeat(7, minmax(30px, 1fr)); }
.daybar__day--wide { min-width: 56px; }
.daybar--month .daybar__day--wide { grid-column: span 2; }

.whenpanel__hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: var(--type-caption);
  color: var(--liquid-secondary-label);
}
.whenpanel__hintaction {
  padding: 0;
  border: 0;
  background: none;
  color: var(--flow-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

/* The preview: the first send and the two after it, or the problem in
 * caution gold (Palette.caution). Empty until /compose/repeat answers. */
.whenpanel__preview {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 10px;
  border-top: 1px solid var(--liquid-separator);
}
.whenpanel__preview:empty { display: none; }
.whenpanel__line { margin: 0; font-size: 14px; color: var(--liquid-primary-label); }
.whenpanel__line--quiet { color: var(--liquid-secondary-label); }
.whenpanel__label { margin-right: 4px; font-weight: 600; color: var(--liquid-secondary-label); }
.whenpanel__problem { margin: 0; font-size: 14px; font-weight: 600; color: #8F6500; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .whenpanel__problem { color: #F4C542; }
}
:root[data-theme="dark"] .whenpanel__problem { color: #F4C542; }

/* The detail page's whole repeat, under the clock, where the rows cannot say
 * it — "Sends every 2 weeks on Sun, Sat at 8:00 AM and 6:00 PM, 6 times". */
.sched__rule {
  margin: -6px 0 0;
  padding: 0 14px 12px;
  font-size: var(--type-small);
  color: var(--liquid-secondary-label);
}

/* ── THE PARITY AUDIT, 2026-10-04 ─────────────────────────────────────────── */

/* THE CONFIRM DIALOG (layout.html #confirm-dlg): the app's ping dialog — the
 * question, then Cancel and the verb, trailing — in the share panel's box.
 * It replaced window.confirm, which spoke the browser's language. */
.confirmdlg {
  width: min(400px, calc(100vw - 32px));
  padding: 20px;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: var(--liquid-radius-overlay, 28px);
  background: var(--liquid-content-surface);
  color: var(--liquid-primary-label);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.24);
}
.confirmdlg::backdrop { background: rgba(0, 0, 0, 0.36); }
.confirmdlg__text { margin: 0; font-size: 17px; line-height: 1.4; font-weight: 600; overflow-wrap: anywhere; }
.confirmdlg__actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; margin-top: 20px; }

/* BUSY WHILE A SWAP IS IN FLIGHT (layout.html sets aria-busy on the region an
 * htmx request will replace). Dimmed after .15s, so an answer that comes back
 * at once never flickers; a form being sent keeps its look and shows the
 * cursor only, since its fields are what the reader is looking at. */
main [aria-busy="true"]:not(form) {
  opacity: 0.55;
  transition: opacity 0.12s ease 0.15s;
  cursor: progress;
}
form[aria-busy="true"] [type="submit"] { cursor: progress; opacity: 0.7; }
@media (prefers-reduced-motion: reduce) {
  main [aria-busy="true"]:not(form) { transition: none; }
}

/* 44PX TO THE FINGER, NOT TO THE EYE. The card's ⋯ (30), a reaction pill (24
 * tall) and a head chip off the phone layout (36) keep the size they are
 * drawn at; on a touch screen an invisible box round each takes the press
 * out to the 44px floor, as the app's tap targets are. Not on a mouse, where
 * the box would overlap whatever sits beside the control. */
@media (pointer: coarse) {
  .cardmenu__btn,
  .pill-react,
  .chipbtn:not(.chipbtn--me) { position: relative; }
  .cardmenu__btn::after,
  .pill-react::after,
  .chipbtn:not(.chipbtn--me)::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
  }
}

/* THE WIDE SCREEN. One breakpoint past the phone's: from 1200px the signed-in
 * column widens from 640 to 760, so a desktop window is not two 500px bands
 * round a phone-width strip. The phone and tablet layouts are untouched, and
 * nothing is rearranged — the same column, wider. */
@media (min-width: 1200px) {
  .shell .page { max-width: 760px; }
}

/* ── ATTACHED LINKS AND VOICE NOTES, AS LISTS (2026-10-05) ─────────────────
 *
 * Several of each may be attached now, so each run is ONE list of even rows —
 * the way a run of photographs is one strip — rather than a column of unrelated
 * cards. Every row is the same height whatever it points at: a 40px squircle
 * glyph tile where a picture would be, the title, the host under it. An
 * attached link never draws a picture (the user's rule), YouTube included.
 *
 * Five rows, then the rest behind a <details> that opens IN PLACE. Never an
 * inner scroller: it would trap the finger and fight the page's own scroll.
 * The toggle hides once open — "Show all" has nothing left to show. */
.attachlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-row);
}
.blocks > .attachlist,
.blocks > .attachlist__more { margin: 0 0 16px; }
.attachlist__more > .attachlist { margin-top: var(--space-row); }

.attachrow {
  display: flex;
  align-items: center;
  gap: var(--space-inner);
  min-height: 56px;
  padding: 8px 12px;
  border: var(--liquid-hairline) solid var(--liquid-separator);
  border-radius: 14px;
  text-decoration: none;
  color: inherit;
}
a.attachrow:hover { background: var(--liquid-fill-quiet); }
.attachrow__glyph {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  display: grid;
  place-items: center;
  background: var(--liquid-fill-quiet);
  color: var(--liquid-accent);
}
.attachrow__glyph svg { width: 20px; height: 20px; }
.attachrow__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.attachrow__title {
  font-weight: 600;
  color: var(--liquid-accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attachrow__sub {
  color: var(--liquid-secondary-label);
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attachrow--voice .attachrow__title { color: var(--liquid-primary-label); }
.blocks .attachrow audio { margin: 4px 0 0; }

.attachlist__toggle {
  cursor: pointer;
  list-style: none;
  color: var(--liquid-accent);
  font-weight: 600;
  padding: 8px 0;
}
.attachlist__toggle::-webkit-details-marker { display: none; }
.attachlist__more[open] > .attachlist__toggle { display: none; }

/* ── The composer's attachments, grouped (2026-10-05) ───────────────────────
 * One run per kind in the order each was first attached, as PingComposer
 * draws them: the photo strip, then columns of link, voice and place rows in
 * the reading surface's .attachrow shape with ✕ at the end. Every row of a
 * run of two or more drags within its run. The header carries the slots spent
 * ("3/20"). The link field is the app's Attach a link sheet, inline. */
.attach__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.attach__count {
  margin: 16px 4px 0;
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--liquid-secondary-label);
}
.attach__count[hidden] { display: none; }
.attach__runs { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; }
.attach__runs:empty { display: none; }
.attach__runs > .attach__strip { margin-top: 0; }
.attachlist--compose .attachrow { position: relative; padding-right: 44px; background: var(--liquid-content-surface); }
.attachlist--compose .attachrow .attach__remove {
  top: 50%;
  transform: translateY(-50%);
  background: var(--liquid-fill-quiet);
  color: var(--liquid-primary-label);
}
.attachlist--compose .attachrow__glyph { color: var(--liquid-secondary-label); }
.attachlist--compose .attachrow__title { color: var(--liquid-primary-label); }
.attachlist--compose .attachrow audio { width: 100%; height: 36px; margin-top: 4px; }
.attachrow--action { cursor: pointer; }
.attachrow--action:hover, .attachrow--action:focus-visible { background: var(--liquid-fill-quiet); }
.attachrow[draggable="true"] { cursor: grab; }
.attachrow--pending {
  min-height: 56px;
  background: var(--liquid-fill-quiet);
  animation: attach-pulse 1.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .attachrow--pending { animation: none; } }
.attach--dragging { opacity: 0.5; }

.attach__linkform {
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: var(--liquid-radius-card);
  background: var(--liquid-content-surface);
  border: var(--liquid-hairline) solid var(--liquid-separator);
}
.attach__linkform[hidden] { display: none; }
.attach__linkrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 12px;
  background: var(--liquid-fill-quiet);
}
.attach__linkrow .composer__linkinput { flex: 1 1 auto; min-width: 0; }
.attach__linkhelp {
  margin: 6px 4px 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attach__linkhelp--ok { color: var(--flow-accent); }
.attach__linkhelp--bad { color: var(--liquid-destructive); white-space: normal; }
.attach__linkactions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }

/* A watched YouTube / Twitch channel's own picture (2026-10-05): on the
 * create form's channel row once chosen, and under CHANNEL TO WATCH on the
 * ping's page. Round, with the glyph on a tint while there is none. */
.summaryrow__lead--picture img,
.watchpic img { display: block; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.summaryrow__lead--picture { width: 36px; height: 36px; }
.watchpic {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;
  color: var(--flow-accent, var(--liquid-accent));
  background: var(--liquid-fill-quiet);
}
.watchpic .nav__glyph { width: 20px; height: 20px; }

/* ── SENDS AND WHAT CAME BACK (2026-10-06, user) ─────────────────────────────
 * History's line under a send — the app's FiringAnswersLine: a chip per kind
 * of answer beside the emoji pill — and the same chips as the send page's
 * filter strip (ReplyKindChip). */
.kindchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.kindchips:empty { display: none; }
.kindchips--strip { margin: 0 0 12px; }
.kindchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 26px;
  padding: 0 10px;
  border-radius: 999px;
  background: var(--liquid-fill-quiet);
  color: var(--liquid-secondary-label);
  font-size: var(--type-small);
  text-decoration: none;
}
.kindchip .nav__glyph { width: 14px; height: 14px; }
.kindchip__count { font-variant-numeric: tabular-nums; }
.kindchip--on { background: var(--liquid-selected-fill); color: var(--liquid-selected-ink); }

/* One answer on the send page: a text reply's words, or the photo, voice note
 * or place drawn as ping blocks. The person a reaction notice named is
 * tinted, as the app's highlighted row. */
.answer__body { display: block; margin-top: 4px; white-space: pre-wrap; overflow-wrap: anywhere; }
.answer__blocks { display: block; margin-top: 8px; }
.answer--marked { background: var(--liquid-selected-fill); }

/* A reaction notice in the Inbox: the emoji on a line of its own, then what it
 * was left on, quoted and muted under it (2026-10-07, user).
 * RETIRED 2026-10-07 — both on one line:
 *   .nrow__reaction { display: flex; align-items: center; gap: 8px; margin-top: 4px; min-width: 0; }
 */
.nrow__reactionglyph { display: block; margin-top: 4px; font-size: 22px; line-height: 1.2; }
.nrow__reactionctx {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  color: var(--liquid-secondary-label);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.ncard--read .nrow__reactionctx { opacity: 0.62; }

/* WHO IT IS FROM, the header (2026-10-06/07): the name semibold, then "(You)",
 * "@ana" or "• Credits added" regular and lighter, on the same line. */
.nrow__headertail { font-weight: 400; color: var(--liquid-secondary-label); }

/* A reaction notice wears the channel screen's Reactions-row colour, for both
 * kinds (2026-10-07, user) — see notificationGlyphToken. */
.nrow--reaction { --acard-accent: var(--liquid-channel-accent); --acard-tint: rgba(191, 92, 8, 0.14); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .nrow--reaction { --acard-tint: rgba(255, 162, 79, 0.22); }
}
:root[data-theme="dark"] .nrow--reaction { --acard-tint: rgba(255, 162, 79, 0.22); }

/* ── EVERY ATTACHMENT IN ONE STRIP (2026-10-07, user) ────────────────────────
 * PingAttachmentStrip: tiles of one 76px height in one row — a photo at its
 * own shape, a voice note 52 wide, a link 96, a place 76 — on the row glyph's
 * wash. It fits and never scrolls: what does not fit is clipped, and the card
 * stays one tap. */
.astrip {
  display: flex;
  gap: 6px;
  margin-top: 10px;
  height: 76px;
  overflow: hidden;
  max-width: 100%;
}
.astrip__tile {
  position: relative;
  flex: 0 0 auto;
  height: 76px;
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: 10px 8px 8px;
  color: var(--acard-accent);
  background: var(--acard-tint);
}
.astrip__tile--photo { padding: 0; background: var(--liquid-fill-quiet); }
.astrip__tile--voice { width: 52px; }
.astrip__tile--link { width: 96px; align-items: flex-start; padding: 10px; }
.astrip__tile--place { width: 76px; }
.astrip__img { display: block; height: 76px; width: auto; max-width: 160px; min-width: 34px; object-fit: cover; }
.astrip__glyph { display: inline-flex; width: 20px; height: 20px; }
.astrip__glyph .nav__glyph { width: 20px; height: 20px; }
.astrip__tile--link .astrip__glyph,
.astrip__tile--link .astrip__glyph .nav__glyph { width: 16px; height: 16px; }
.astrip__len { font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; }
.astrip__text {
  font-size: 11px;
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.astrip__tile--link .astrip__text { font-size: 12px; text-align: left; }
.astrip__more {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  font-weight: 600;
  color: #fff;
  background: rgba(0, 0, 0, 0.45);
}
.ncard--read .astrip { opacity: 0.62; }

/* ── THE NOTIFICATION'S BOTTOM BAR (2026-10-06) ─────────────────────────────
 * _NotificationActionBar: Delete and React on one grey capsule under the
 * page, the glyph over a small caption, as .actionbar draws the ping view's.
 * In the flow rather than fixed: it is the page's last thing on every width.
 * React's panel opens upward over the capsule. */
.nbar {
  display: flex;
  justify-content: center;
  gap: 4px;
  width: max-content;
  max-width: 100%;
  margin: 28px auto 8px;
  padding: 4px 8px;
  border-radius: 28px;
  background: var(--liquid-flat-chrome-fill);
  border: 1px solid var(--liquid-content-border);
}
.nbar form { margin: 0; }
.nbar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 64px;
  height: 52px;
  padding: 0 10px;
  border: 0;
  border-radius: 22px;
  background: none;
  color: var(--liquid-secondary-label);
  font: inherit;
  cursor: pointer;
  list-style: none;
}
.nbar__item::-webkit-details-marker { display: none; }
.nbar__item:hover { background: var(--liquid-fill-quiet); }
.nbar__glyph { display: inline-flex; width: 21px; height: 21px; align-items: center; justify-content: center; }
.nbar__glyph .nav__glyph { width: 21px; height: 21px; }
.nbar__emoji { font-size: 19px; line-height: 1; }
.nbar__label { font-size: 10px; font-weight: 600; }
.nbar__react { position: relative; }
.nbar__react > .cardmenu__panel { bottom: calc(100% + 8px); top: auto; left: 50%; right: auto; transform: translateX(-50%); }

/* A social firing's watched channel, over the panel's headline (2026-10-06):
 * 13 semibold in the panel's accent. */
.rxpanel__kicker { margin: 0 0 2px; font-size: 13px; font-weight: 600; color: var(--acard-accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── WHO A SEND WENT TO (2026-10-07) — SendDeliveriesScreen's _SentOn ───────
 * One squircle per delivery method on the right of a person's row, the
 * delivery chooser's shape: the selected colour washed behind its glyph. A
 * method that did not reach them is red, the glyph faded, struck through. */
.sentmarks { display: inline-flex; gap: 6px; }
.sentmark {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  color: var(--liquid-selected-ink);
  background: var(--liquid-selected-fill);
  border: 1px solid color-mix(in srgb, currentColor 75%, transparent);
}
.sentmark .nav__glyph { width: 14px; height: 14px; }
.sentmark--failed { color: var(--liquid-destructive-icon); background: color-mix(in srgb, var(--liquid-destructive-icon) 9%, transparent); }
.sentmark--failed .nav__glyph { opacity: 0.35; }
.sentmark--failed::before,
.sentmark--failed::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 14px;
  height: 1.8px;
  border-radius: 1px;
  background: currentColor;
  transform: translate(-50%, -50%) rotate(45deg);
}
.sentmark--failed::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* ── A DAILY LIMIT, INLINE (2026-10-07) — UpdateLimitTile ───────────────────
 * − the number + at the row's end, ∞ for none; the number opens the sheet to
 * type one. Each step is its own little form. */
.limitctl { display: inline-flex; align-items: center; gap: 2px; }
.limitctl form { margin: 0; display: inline-flex; }
.limitctl__btn {
  width: 32px;
  height: 32px;
  border-radius: 16px;
  border: 0;
  background: color-mix(in srgb, var(--liquid-channel-accent) 12%, transparent);
  color: var(--liquid-channel-accent);
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
}
.limitctl__btn:disabled { opacity: 0.35; cursor: default; }
.limitctl__value {
  min-width: 44px;
  border: 0;
  background: none;
  color: var(--liquid-primary-label);
  font: inherit;
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
  cursor: pointer;
}
/* The Delivery row's cost, "2 credits", in the caution colour. */
.chcost { font-size: 15px; font-weight: 600; color: #9E6F00; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .chcost { color: #F4C542; } }
:root[data-theme="dark"] .chcost { color: #F4C542; }

/* The pill's face on a row that is a link (reaction-counts): no tap of its
 * own, so no pointer. */
.pill-react--static { cursor: inherit; }

/* React on the channel capsule: its panel opens upward over the bar. */
.actbar__react { position: relative; }
.actbar__react > summary { list-style: none; }
.actbar__react > summary::-webkit-details-marker { display: none; }
.actbar__react > .cardmenu__panel { bottom: calc(100% + 8px); top: auto; left: 50%; right: auto; transform: translateX(-50%); }

/* A card narrowed to one channel heads itself with its kind, "YouTube
 * update" (2026-10-07): the channel name's weight, no picture. */
.pcard__kind { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 15px; color: var(--liquid-primary-label); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* …led by its source mark (2026-10-08) — the app's PingSourceMark: a 28
 * square, radius 28 × 0.28, the ping blue at 14% (22% dark) with the glyph
 * at half size. Every source wears the ping blue in the app, so one rule.
 * The head is centred on the mark when it has one: flex-start would hang the
 * 15px title off the square's top edge. */
.srcmark { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 8px; color: #2D4F9E; background: rgba(45, 79, 158, 0.14); }
.srcmark .nav__glyph { width: 14px; height: 14px; stroke-width: 2; }
/* RETIRED: .pcard__head:has(> .srcmark) { align-items: center; } — the
 * attributed head carries the mark inside its link now, so not a child. */
.pcard__head:has(.srcmark) { align-items: center; }
/* The attributed name beside the mark: the app's itemHeadline, 15/600, the
 * same as .pcard__kind — one head row, whichever it names. */
.pcard__channel:has(> .srcmark) { gap: 10px; }
.pcard__channel > .pcard__name { font-size: 15px; }
/* THE CHANNEL'S PICTURE ON THE ATTRIBUTED HEAD (2026-10-08) — at the source
 * mark's 28px and corner, so swapping one for the other moves nothing. */
.pcard__avatar--byline { width: 28px; height: 28px; border-radius: 8px; font-size: 13px; }
.pcard__head:has(.pcard__avatar--byline) { align-items: center; }
.pcard__channel:has(> .pcard__avatar--byline) { gap: 10px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .srcmark { color: #7C9CFF; background: rgba(124, 156, 255, 0.22); }
}
:root[data-theme="dark"] .srcmark { color: #7C9CFF; background: rgba(124, 156, 255, 0.22); }

/* An empty circle while Now is chosen (2026-10-07): stepped back, with the
 * reason under its name. */
.trow--locked { opacity: 0.55; cursor: not-allowed; }
.trow__lockednote { white-space: normal; }

/* ── A CHANNEL'S WAYS OF GETTING IT (2026-10-09, _deliveryRows) ──────────────
 * App / Email rows under NOTIFICATIONS: where it lands is ONE line, dragged
 * sideways when long (the app's horizontal scroll, no wrap, no ellipsis), and
 * the filter before the switch is the app's DeliveryChooserButton — a 36pt
 * target round an 18pt `tune` mark. */
.chmethod__where {
  display: block;
  white-space: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
.chmethod__where::-webkit-scrollbar { display: none; }
.chmethod__filter {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-right: 4px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: none;
  color: var(--liquid-secondary-label);
  cursor: pointer;
}
.chmethod__filter .nav__glyph { width: 18px; height: 18px; }
.chmethod__filter:hover { color: var(--liquid-primary-label); }

/* NO DESCRIPTION (2026-10-08): "No description provided" in the tertiary
 * grey for a reader, "Add a description" in the channel's colour for the
 * owner. :root lifts it over the dark-mode .chead__desc colour above. */
:root .chead__desc.chead__desc--none { color: var(--liquid-tertiary-label); }
.chead__adddesc { margin-top: 14px; font-size: 15px; }

/* A CHANNEL POST CARD'S ATTACHMENT STRIP (2026-10-09) — the Inbox's .astrip in
 * the channel's colours (.nrow--channel's), 12 under the body as the app's card. */
.pcard__strip { --acard-accent: var(--liquid-channel-accent); --acard-tint: rgba(191, 92, 8, 0.14); }
.pcard__strip .astrip { margin-top: 12px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pcard__strip { --acard-tint: rgba(255, 162, 79, 0.22); }
}
:root[data-theme="dark"] .pcard__strip { --acard-tint: rgba(255, 162, 79, 0.22); }

/* THE INBOX ROW'S MARK (2026-10-09, inbox_row.dart) — where it came from, in the
 * glyph's 36px place: a person round (their photo, else their initial on a
 * tint), a channel in its squircle, Aletto as the app icon, round. */
.nrow__mark {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  overflow: hidden;
}
.nrow__mark--channel { border-radius: 28%; }
.ncard--read .nrow__mark { opacity: 0.62; }
/* The Aletto mark in a glyph's place (icon-aletto): the app icon, round. */
.alettomark { border-radius: 50%; object-fit: cover; }

/* A CHANNEL AS A ROW (2026-10-09) — a person's CHANNELS: the picture in the row's
 * 36px leading place, in the channel squircle, the initial on its tint without one. */
.row__avatar {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  object-fit: cover;
  font-size: 15px;
  font-weight: 700;
  color: #fff;
}
.row__avatar--channel { border-radius: 28%; }
/* The answer to "@ana wants to send you pings" — Allow and Decline under it. */
.personask { display: flex; gap: 8px; margin-top: 10px; }

/* A FROM ROW THROUGH A CIRCLE (2026-10-08) — the circle's emblem at the right,
 * 32px, as the app's trailing circleBadge. */
.pingfrom__circle { flex: 0 0 auto; display: inline-flex; width: 32px; height: 32px; margin-left: 8px; }
.pingfrom__circle .emblem, .pingfrom__circle .emblem__svg { width: 100%; height: 100%; }

/* DEVICES & EMAILS (2026-10-08) — a grouped row per device or address: the
 * name and its line dragged sideways (.chmethod__where), then the trash and
 * the switch close together. A stopped phone's glyph is grey, an unverified
 * address's amber (.row__icon--caution); the add row is in the brand. */
.dmrow__icon--dim { --tint: var(--liquid-tertiary-label); }
.dmrow .dmcard__trash { margin-left: 4px; }
/* ── RETIRED — .dmrow__switch (2026-10-13) ───────────────────────────────────
 * The gap before a device's or address's "Receive my pings" switch. The
 * switch went with it: your own pings are MY PINGS on the page now.
 *   .dmrow__switch { margin-left: 4px; } */
.dmrow__resend { margin-top: 4px; }
.dmrow__add .row__title { color: #0C8863; font-weight: 500; }
