/* ============================================================================
   fluxy.css — the FluxyCo v2 design system (M4 UX pass)
   ============================================================================

   WHAT THIS FILE IS. The single stylesheet for every v2 page: search, auth,
   item editor, item show. Hand-written, no build step, served as-is. The
   M4.7 baseline established the @layer skeleton and the frozen tokens; this
   pass turns it into the actual design language.

   DESIGN STANCE, in four lines:
   - This is a dense working tool (one user, ~33k items), not a landing
     page: rows are compact, chrome is quiet, the omnibox is the hero.
   - Colour literals live ONLY in the :root token blocks below. Rules speak
     in semantic tokens (--fc-link = interactive text, --fc-warn-* =
     archived/notice, --fc-ok-* = done/saved, --fc-pub-* = public).
   - Dark mode is a token remap under prefers-color-scheme — zero JS, zero
     extra selectors outside the tokens layer.
   - Touch targets are enforced through --fc-tap: compact (28px) on fine
     pointers, 44px on coarse ones, via one media query on the token.

   CSP: SecurityHeadersMiddleware ships a policy with no 'unsafe-inline'.
   A style="" attribute is silently dead in production; everything visual
   is a class in this file. AssertsCspClean greps rendered pages for it.

   TOKENS. The brand palette and the five-step due-date urgency ramp
   (past / today / near / week / far) come from the legacy stylesheet
   my.i14b.com/…/templateDefault/style2024.css and keep their legacy class
   names — the band names and boundaries are a frozen contract; only their
   look is owned here.

   TESTED SHAPES — keep these greppable, the suite asserts on them:
   - `.fc-item-actions {` must contain a fractional opacity (never 0), and
     `.fc-item-actions:focus-within` must restore opacity: 1.
     (testActionsAreDimmedNotHiddenAndComeBackOnFocusWithin)

   @layer ORDER. tokens < base < layout < components. Later layers win
   regardless of specificity, so a component rule never needs an escalating
   selector or !important to beat a base rule.
   ========================================================================== */

@layer tokens, base, layout, components;

/* ── tokens ───────────────────────────────────────────────────────────── */
@layer tokens {
  :root {
    color-scheme: light;

    /* Brand.
       --fc-primary is a SURFACE colour (button fills, current page) and
       always pairs with --fc-on-primary. --fc-link is interactive TEXT and
       borders on light surfaces. They are the same hue in light mode and
       deliberately diverge in dark mode, where indigo-600 stays a fine
       fill but is far too dark to read as text on slate. */
    --fc-primary:        #4f46e5;   /* indigo-600 */
    --fc-primary-hover:  #4338ca;   /* indigo-700 */
    --fc-primary-light:  #eef2ff;   /* indigo-50 — subtle highlight bg */
    --fc-primary-ring:   rgba(79, 70, 229, .35);
    --fc-on-primary:     #ffffff;
    --fc-link:           #4f46e5;
    --fc-link-hover:     #4338ca;

    /* The ground the logo artwork sits on. The mark is a scanned lockup
       with an opaque white background of its own (assets/img/logo-mark.webp
       — see layout/_nav.phtml), so it needs a white chip UNDER it rather
       than a transparent cut-out: the white hairlines BETWEEN the petals
       are part of the drawing, and knocking the background out would knock
       them out too and fuse the four petals into one blob.
       Deliberately NOT remapped in dark mode: white is the artwork's own
       ground in both schemes. On the light nav the chip is invisible
       against --fc-surface; on the dark one it reads as a brand chip. */
    --fc-brand-tile:     #ffffff;

    /* emerald-700/800, not the legacy emerald-600: white 16px text on
       #059669 measures 3.8:1 — below AA. One shade darker passes (5.5:1)
       and reads identically as "the green action". */
    --fc-accent:         #047857;   /* emerald-700 — "New", positive actions */
    --fc-accent-hover:   #065f46;   /* emerald-800 */

    /* Surfaces & text */
    --fc-bg:             #f8fafc;   /* slate-50 */
    --fc-surface:        #ffffff;
    --fc-border:         #e2e8f0;   /* slate-200 */
    --fc-border-strong:  #cbd5e1;   /* slate-300 — hover edges, inputs */
    --fc-text:           #1e293b;   /* slate-800 */
    --fc-text-muted:     #64748b;   /* slate-500 — 4.7:1 on white, AA */

    /* Status families — each is a bg/border/text triple so a pill, badge
       and flash built from the same family always agree. */
    --fc-danger:         #dc2626;   /* red-600 */
    --fc-danger-hover:   #b91c1c;
    --fc-danger-bg:      #fef2f2;
    --fc-danger-text:    #991b1b;

    --fc-ok-bg:          #ecfdf5;
    --fc-ok-border:      #059669;
    --fc-ok-text:        #065f46;

    --fc-warn-bg:        #fffbeb;
    --fc-warn-border:    #d97706;
    --fc-warn-text:      #92400e;

    --fc-pub-bg:         #f5f3ff;
    --fc-pub-border:     #7c3aed;
    --fc-pub-text:       #5b21b6;

    --fc-tag-bg:         #f1f5f9;   /* slate-100 */
    --fc-tag-text:       #475569;   /* slate-600 */

    /* Due-date proximity ramp — five bands, urgent (red) → relaxed
       (green). past = overdue · today = within ~48 h · near = 2-7 d ·
       week = 8-14 d · far = beyond. Class names are frozen legacy. */
    --fc-due-past-bg:      #fef2f2;
    --fc-due-past-border:  #ef4444;
    --fc-due-past-text:    #991b1b;

    --fc-due-today-bg:     #fff7ed;
    --fc-due-today-border: #f97316;
    --fc-due-today-text:   #9a3412;

    --fc-due-near-bg:      #fefce8;
    --fc-due-near-border:  #eab308;
    --fc-due-near-text:    #713f12;

    --fc-due-week-bg:      #f7fee7;
    --fc-due-week-border:  #84cc16;
    --fc-due-week-text:    #365314;

    --fc-due-far-bg:       #f0fdf4;
    --fc-due-far-border:   #22c55e;
    --fc-due-far-text:     #14532d;

    /* Public / visibility badges (legacy green identity) */
    --fc-public-badge-color: #16a34a;
    --fc-public-badge-bg:    #f0fdf4;
    --fc-public-item-border: #22c55e;

    /* Rich-text colour swatches (rich-editor.js TEXT_COLOURS). Text, not
       fills: every value is >= 4.5:1 on --fc-surface in both schemes. */
    --fc-ink-red:    #dc2626;   /* red-600 */
    --fc-ink-blue:   #2563eb;   /* blue-600 */
    --fc-ink-green:  #15803d;   /* green-700 */
    --fc-ink-amber:  #b45309;   /* amber-700 */
    --fc-ink-purple: #7c3aed;   /* violet-600 */

    /* Shape & motion */
    --fc-radius:     6px;
    --fc-radius-lg:  10px;
    --fc-shadow:     0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .04);
    --fc-shadow-lg:  0 4px 12px rgba(0, 0, 0, .08);
    --fc-transition: 150ms ease;

    /* Type scale — three working sizes below the 1rem body. Anything
       smaller than --fc-fs-xs is illegible; do not invent a fourth. */
    --fc-fs-xs:   .75rem;    /* 12px — badges, meta line */
    --fc-fs-sm:   .8125rem;  /* 13px — actions, chips, hints, pills */
    --fc-fs-md:   .9375rem;  /* 15px — secondary body text */
    /* The one step ABOVE the 1rem body, not a fourth small one: the
       editor's title field and any single field that has to read as the
       headline of its form. */
    --fc-fs-lg:   1.125rem;  /* 18px */

    /* Minimum hit size for every standalone control. Compact on a laptop
       (density is the point of this tool), 44px where the pointer is a
       finger — one token, one media query, enforced everywhere. */
    --fc-tap: 1.75rem;

    /* The reading column (search, show); the editor widens it. Percentages,
       not a fixed measure: the content tracks the viewport and keeps a
       margin on each side.
       MOBILE-FIRST since the M-mobile pass: a phone has no width to give
       away, so the default is the whole viewport and the 5%/2.5% side
       margins are added back at the one breakpoint below. The old 90%
       default cost a 375px phone 37px of screen for nothing. */
    --fc-column:      100%;
    --fc-column-wide: 100%;

    /* The shell's side inset, and the ONE value anything that wants to
       bleed to the shell's edge (the sticky search header) negates. Two
       hard-coded 1rems drift; a token cannot. */
    --fc-gutter: .75rem;

    /* Height of the sticky app bar (layout/_nav.phtml). Read by the search
       header's `top`, so the two sticky layers stack with no seam and no
       magic number in two places. It is a FIXED height on purpose — see
       the nav block's banner for why the bar may not grow on mobile. */
    --fc-appbar-h: 3.25rem;

    --fc-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --fc-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  }

  /* ═══ THE ONE BREAKPOINT: 54rem (864px) ═══════════════════════════════
     Written mobile-first everywhere — the small layout is the DEFAULT and
     this query adds the roomy one — so no rule ever has to be undone at
     the other end, and no width matches two contradicting blocks the way
     a max-width/min-width PAIR does at its shared edge.

     WHY 54rem AND NOT A ROUNDER 48. The number is derived, not chosen: it
     is the narrowest width at which the app bar's widest possible content
     — the brand lockup plus all NINE top-level entries an ADMIN sees
     (Home · New · Today · Todo · Contacts · Groups · More · View · Admin,
     ~800px of pills at --fc-fs-md) — still fits on ONE line. That matters
     beyond looks: `.fc-header` sticks at exactly --fc-appbar-h, so a bar
     that silently grew to two rows would slide under its own second row.
     Above this width the bar is not sticky at all and the constraint is
     lifted (see the GLOBAL NAVIGATION banner).

     It is in rem, not px, so a reader who scales their root font gets the
     drawer for longer rather than a bar that overflows.

     ONE breakpoint for the whole file. `.editor-dialog` further down still
     has its own 46rem query; that one is about a dialog's own geometry,
     not about the page's layout mode, and is left alone. */
  @media (min-width: 54rem) {
    :root {
      --fc-column:      90%;
      --fc-column-wide: 95%;
      --fc-gutter:      1rem;
    }
  }

  @media (pointer: coarse) {
    :root { --fc-tap: 2.75rem; }
  }

  /* Dark mode = a token remap, nothing else. Fills (--fc-primary,
     --fc-accent, --fc-danger) keep their light-mode values — white text on
     them passes either way — while text-ish tokens lighten.

     Two ways in, SAME tokens: the OS (`prefers-color-scheme`, unless the
     user pinned Light) and an explicit `data-theme="dark"` that theme.js
     sets from the per-device choice on /account/settings. The token list
     therefore appears twice — CSS cannot share one block between a media
     query and an attribute selector with no build step — and
     ThemeTokensTest fails if the two copies drift. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      color-scheme: dark;

      --fc-primary-light:  #1e1b4b;
      --fc-primary-ring:   rgba(129, 140, 248, .4);
      --fc-link:           #a5b4fc;   /* indigo-300 — 7.6:1 on slate-900 */
      --fc-link-hover:     #c7d2fe;

      --fc-bg:             #0f172a;   /* slate-900 */
      --fc-surface:        #1e293b;   /* slate-800 */
      --fc-border:         #334155;   /* slate-700 */
      --fc-border-strong:  #475569;
      --fc-text:           #e2e8f0;   /* slate-200 */
      --fc-text-muted:     #94a3b8;   /* slate-400 — 5.4:1 on slate-800 */

      --fc-danger-bg:      #2b1214;
      --fc-danger-text:    #fca5a5;

      --fc-ok-bg:          #10231a;
      --fc-ok-border:      #34d399;
      --fc-ok-text:        #6ee7b7;

      --fc-warn-bg:        #2a2008;
      --fc-warn-border:    #f59e0b;
      --fc-warn-text:      #fcd34d;

      --fc-pub-bg:         #211836;
      --fc-pub-border:     #8b5cf6;
      --fc-pub-text:       #c4b5fd;

      --fc-tag-bg:         #334155;
      --fc-tag-text:       #cbd5e1;

      --fc-due-past-bg:      #2b1214;
      --fc-due-past-border:  #f87171;
      --fc-due-past-text:    #fca5a5;

      --fc-due-today-bg:     #2a190b;
      --fc-due-today-border: #fb923c;
      --fc-due-today-text:   #fdba74;

      --fc-due-near-bg:      #272108;
      --fc-due-near-border:  #eab308;
      --fc-due-near-text:    #fde047;

      --fc-due-week-bg:      #1a230d;
      --fc-due-week-border:  #a3e635;
      --fc-due-week-text:    #bef264;

      --fc-due-far-bg:       #10231a;
      --fc-due-far-border:   #4ade80;
      --fc-due-far-text:     #86efac;

      --fc-public-badge-color: #4ade80;
      --fc-public-badge-bg:    #10231a;

      --fc-ink-red:    #f87171;
      --fc-ink-blue:   #60a5fa;
      --fc-ink-green:  #4ade80;
      --fc-ink-amber:  #fbbf24;
      --fc-ink-purple: #a78bfa;

      --fc-shadow:     0 1px 3px rgba(0, 0, 0, .4);
      --fc-shadow-lg:  0 4px 12px rgba(0, 0, 0, .5);
    }
  }

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

    --fc-primary-light:  #1e1b4b;
    --fc-primary-ring:   rgba(129, 140, 248, .4);
    --fc-link:           #a5b4fc;   /* indigo-300 — 7.6:1 on slate-900 */
    --fc-link-hover:     #c7d2fe;

    --fc-bg:             #0f172a;   /* slate-900 */
    --fc-surface:        #1e293b;   /* slate-800 */
    --fc-border:         #334155;   /* slate-700 */
    --fc-border-strong:  #475569;
    --fc-text:           #e2e8f0;   /* slate-200 */
    --fc-text-muted:     #94a3b8;   /* slate-400 — 5.4:1 on slate-800 */

    --fc-danger-bg:      #2b1214;
    --fc-danger-text:    #fca5a5;

    --fc-ok-bg:          #10231a;
    --fc-ok-border:      #34d399;
    --fc-ok-text:        #6ee7b7;

    --fc-warn-bg:        #2a2008;
    --fc-warn-border:    #f59e0b;
    --fc-warn-text:      #fcd34d;

    --fc-pub-bg:         #211836;
    --fc-pub-border:     #8b5cf6;
    --fc-pub-text:       #c4b5fd;

    --fc-tag-bg:         #334155;
    --fc-tag-text:       #cbd5e1;

    --fc-due-past-bg:      #2b1214;
    --fc-due-past-border:  #f87171;
    --fc-due-past-text:    #fca5a5;

    --fc-due-today-bg:     #2a190b;
    --fc-due-today-border: #fb923c;
    --fc-due-today-text:   #fdba74;

    --fc-due-near-bg:      #272108;
    --fc-due-near-border:  #eab308;
    --fc-due-near-text:    #fde047;

    --fc-due-week-bg:      #1a230d;
    --fc-due-week-border:  #a3e635;
    --fc-due-week-text:    #bef264;

    --fc-due-far-bg:       #10231a;
    --fc-due-far-border:   #4ade80;
    --fc-due-far-text:     #86efac;

    --fc-public-badge-color: #4ade80;
    --fc-public-badge-bg:    #10231a;

    --fc-ink-red:    #f87171;
    --fc-ink-blue:   #60a5fa;
    --fc-ink-green:  #4ade80;
    --fc-ink-amber:  #fbbf24;
    --fc-ink-purple: #a78bfa;

    --fc-shadow:     0 1px 3px rgba(0, 0, 0, .4);
    --fc-shadow-lg:  0 4px 12px rgba(0, 0, 0, .5);
  }

}

/* ── base ─────────────────────────────────────────────────────────────── */
@layer base {
  *, *::before, *::after { box-sizing: border-box; }

  body {
    margin: 0;
    padding: 0;
    font-family: var(--fc-font);
    font-size: 16px;
    line-height: 1.5;
    color: var(--fc-text);
    background: var(--fc-bg);
  }

  /* The `hidden` attribute must ALWAYS win. Several components (the
     delete-confirm pair, upload thumbs, the omnibox popup, item bodies)
     are toggled by JS through el.hidden, and any component rule that sets
     `display` would silently resurrect them — the baseline's
     `.fc-confirm { display:flex }` did exactly that. This is the one
     sanctioned !important in the file. */
  [hidden] { display: none !important; }

  h1, h2, h3 { line-height: 1.25; margin: 0 0 .5em; }

  a { color: var(--fc-link); text-decoration: none; }
  a:hover { color: var(--fc-link-hover); text-decoration: underline; }

  code {
    font-family: var(--fc-font-mono);
    font-size: .9em;
    background: var(--fc-tag-bg);
    padding: 1px 4px;
    border-radius: 4px;
  }

  /* One visible focus treatment for every interactive element. Never
     removed: keyboard users have no other way to see where they are.
     --fc-link (not --fc-primary) so the ring stays visible in dark mode. */
  a:focus-visible,
  button:focus-visible,
  summary:focus-visible,
  input:focus-visible,
  select:focus-visible,
  textarea:focus-visible,
  .fc-pill-input:focus-visible + .fc-bar-pill {
    outline: 2px solid var(--fc-link);
    outline-offset: 2px;
  }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition-duration: 1ms !important; animation-duration: 1ms !important; }
  }
}

/* ── layout ───────────────────────────────────────────────────────────── */
@layer layout {
  .fc-shell {
    max-width: var(--fc-column);
    margin: 0 auto;
    padding: var(--fc-gutter) var(--fc-gutter) 4rem;
  }

  .fc-shell-wide { max-width: var(--fc-column-wide); }

  /* Sticky search header. The bottom hairline is what stops rows from
     visually bleeding into the omnibox while scrolling — a border, not a
     backdrop-filter, because blur forces a repaint on every scrolled frame
     of a 500-row list.
     ─── IT STICKS UNDER THE APP BAR, NOT UNDER THE VIEWPORT TOP ─────────
     `top: 0` was correct while the nav scrolled away with the page. The
     nav is now a sticky app bar (see the GLOBAL NAVIGATION banner), so a
     second sticky layer at 0 would slide UNDERNEATH it and take the query
     box with it. --fc-appbar-h is the bar's fixed height; the two layers
     are flush because both read the same token.
     ─── AND IT BLEEDS TO THE SHELL'S EDGE ───────────────────────────────
     Negative side margins cancel the shell's gutter and the padding puts
     it back inside, so the opaque band spans the full column. Without it,
     rows scroll past IN the gutter, visible either side of the header —
     which on a phone is most of what the eye catches. The negative
     margin-top does the same job upwards, closing the strip of page
     background between the bar and the header. */
  .fc-header {
    position: sticky;
    top: var(--fc-appbar-h);
    z-index: 10;
    background: var(--fc-bg);
    margin: calc(-1 * var(--fc-gutter)) calc(-1 * var(--fc-gutter)) 0;
    padding: var(--fc-gutter) var(--fc-gutter) .35rem;
    border-bottom: 1px solid var(--fc-border);
  }

  /* Above the breakpoint the app bar stops being sticky (its height is no
     longer guaranteed once the entry row may wrap), so the search header
     goes back to owning the top of the viewport exactly as it did before
     this pass. Desktop scroll behaviour is unchanged by the mobile work. */
  @media (min-width: 54rem) {
    .fc-header { top: 0; }
  }

  .fc-main { margin-top: 1rem; }

  /* Available to a screen reader, invisible on screen. */
  .fc-visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* ── components ───────────────────────────────────────────────────────── */
@layer components {

  /* Omnibox + buttons ---------------------------------------------------- */
  .fc-omnibox-row { display: flex; gap: .5rem; align-items: stretch; }

  /* `font: inherit` resolves to the body's 16px, and that is LOad-BEARING
     on iOS, not a stylistic choice: Mobile Safari zooms the whole document
     when a focused form control computes below 16px, and it does not zoom
     back out. Do not give this field --fc-fs-md "to match the pills". The
     same trap is closed for the smaller controls in the coarse-pointer
     block at the end of this layer. */
  .fc-omnibox {
    flex: 1 1 auto;
    min-width: 0;
    font: inherit;
    padding: .55rem .8rem;
    color: var(--fc-text);
    background: var(--fc-surface);
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius-lg);
    transition: border-color var(--fc-transition), box-shadow var(--fc-transition);
  }
  .fc-omnibox::placeholder { color: var(--fc-text-muted); opacity: .8; }
  .fc-omnibox:focus {
    outline: none;
    border-color: var(--fc-link);
    box-shadow: 0 0 0 3px var(--fc-primary-ring);
  }

  /* Combobox popup — the wrapper is the positioning context; the listbox
     OVERLAYS the page instead of pushing results down, because a dropdown
     that reflows the document on every keystroke moves the row under the
     pointer. The <ul> is toggled via the `hidden` attribute by omnibox.js. */
  /* `flex-basis: 12rem` rather than `auto`: at 360px the row is the field
     plus two buttons, and with an `auto` basis the field is sized from its
     (empty) content and collapses to whatever the buttons leave — around
     150px, which is not enough to read back a query you just typed. A
     basis states the field's claim on the row; `min-width: 0` still lets
     it shrink below it rather than overflowing the page. */
  .fc-combobox { flex: 1 1 12rem; min-width: 0; display: flex; }
  .fc-combobox .fc-omnibox { width: 100%; }

  /* ─── WHICH BOX THE POPUP HANGS OFF, AND WHY IT CHANGES ──────────────
     The popup is `left: 0; right: 0` against its positioning ancestor, so
     that ancestor decides its width. On a phone the field is only part of
     the row — the rest is the submit and New — and anchoring to the field
     gave a 242px popup on a 390px screen: two-thirds of the width, with
     the suggestion descriptions ("due in the next 48 h") wrapping for no
     reason while 120px sat empty beside them.
     Anchoring to the ROW instead spans the popup edge to edge. The row is
     the same height as the field, so `top: calc(100% + .25rem)` still
     lands exactly under it. Above the breakpoint the field is already wide
     and the popup goes back to matching it — a 1100px-wide list of short
     suggestions is a worse read than a 950px one. */
  .fc-omnibox-row { position: relative; }

  @media (min-width: 54rem) {
    .fc-combobox { position: relative; }
  }

  .fc-omnibox-list {
    position: absolute;
    top: calc(100% + .25rem);
    left: 0;
    right: 0;
    z-index: 20;
    margin: 0;
    padding: .25rem;
    list-style: none;
    /* 18rem is 288px — more than the space left above the on-screen
       keyboard on a short phone, so the last suggestions render behind it.
       The dvh half of the min() caps the popup at a share of the CURRENT
       viewport instead. It is a mitigation, not a cure: iOS does not
       shrink the layout viewport for the keyboard, so this needs checking
       on a real handset (see the sign-off list). */
    max-height: min(18rem, 45dvh);
    overflow-y: auto;
    /* Scrolling the suggestions must not chain into scrolling the results
       underneath once the popup hits its end — on touch that reads as the
       page lurching while you are picking a suggestion. */
    overscroll-behavior: contain;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    box-shadow: var(--fc-shadow-lg);
  }

  .fc-omnibox-option {
    display: flex;
    gap: .6rem;
    align-items: baseline;
    padding: .45rem .6rem;
    min-height: var(--fc-tap);
    border-radius: var(--fc-radius);
    cursor: pointer;
  }
  /* The highlight is a CLASS, driven by the same code that writes
     aria-activedescendant, so the visible and the announced selection can
     never disagree. :hover is separate on purpose — mousing over the list
     must not silently change what Enter would accept. */
  .fc-omnibox-option.is-active { background: var(--fc-primary-light); }
  .fc-omnibox-option:hover { background: var(--fc-tag-bg); }
  .fc-omnibox-option-label { font-weight: 600; }
  .fc-omnibox-option-desc { color: var(--fc-text-muted); font-size: var(--fc-fs-sm); }

  .fc-omnibox-hint {
    position: absolute;
    top: calc(100% + .25rem);
    left: 0;
    z-index: 20;
    margin: 0;
    padding: .3rem .6rem;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    box-shadow: var(--fc-shadow);
  }

  /* Buttons -------------------------------------------------------------- */
  .fc-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    font: inherit;
    font-weight: 500;
    line-height: 1.2;
    min-height: var(--fc-tap);
    padding: .45rem .9rem;
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    color: var(--fc-text);
    cursor: pointer;
    text-decoration: none;
    transition: background var(--fc-transition), border-color var(--fc-transition);
  }
  .fc-btn:hover { text-decoration: none; border-color: var(--fc-link); color: var(--fc-text); }

  .fc-btn-primary { background: var(--fc-primary); border-color: var(--fc-primary); color: var(--fc-on-primary); }
  .fc-btn-primary:hover { background: var(--fc-primary-hover); border-color: var(--fc-primary-hover); color: var(--fc-on-primary); }

  .fc-btn-accent { background: var(--fc-accent); border-color: var(--fc-accent); color: var(--fc-on-primary); }
  .fc-btn-accent:hover { background: var(--fc-accent-hover); border-color: var(--fc-accent-hover); color: var(--fc-on-primary); }

  .fc-btn-block { width: 100%; }

  /* The submit button of the search row, small screens ------------------- */

  /* At 360px the row is a query field plus "Search" plus "New" — three
     text controls competing for 336px, and the field loses. The submit
     therefore drops to its magnifier below the breakpoint while KEEPING
     its accessible name: the word is clipped by the same technique
     .fc-visually-hidden uses, so screen readers and voice control still
     hear and match "Search", and the target stays a full --fc-tap square.
     The icon is not decoration standing in for a label — it is a label
     that is only invisible. */
  /* min-width as well as the inherited min-height: without it the button
     is only as wide as its 20px glyph plus padding (~40px), and a control
     whose whole label is invisible is the last one that should be the
     smallest target on the row. */
  .fc-btn-search { padding-inline: .55rem; min-width: var(--fc-tap); }

  .fc-btn-icon {
    width: 1.25rem;
    height: 1.25rem;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .fc-btn-search-text {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  @media (min-width: 54rem) {
    /* Room again: the word comes back and the glyph goes, rather than both
       showing. One label per control. */
    .fc-btn-search { padding-inline: .9rem; }
    .fc-btn-search .fc-btn-icon { display: none; }
    .fc-btn-search-text {
      position: static;
      width: auto; height: auto;
      margin: 0;
      overflow: visible;
      clip-path: none;
    }
  }

  /* Filter pills ----------------------------------------------------------

     ─── WHY THE BAR IS A DISCLOSURE NOW (search/_filters.phtml) ──────────
     Eleven controls at a 44px touch target wrap to five or six rows on a
     phone. They sat INSIDE the sticky header, so on a 375×667 screen the
     permanently-parked chrome measured about 350px — over half the
     viewport — and the result list read through a letterbox. Collapsing
     them behind one summary takes that back to a single 44px row.

     It is a disclosure and not a hidden panel: the summary NAMES the
     filters that are on ("Filters · Updated · Archived"), because a
     collapsed control that hides active state is how a user ends up
     believing their list is broken when it is merely filtered.

     Above the breakpoint the summary is gone and the bar is simply open —
     see THE DESKTOP UN-COLLAPSE at the end of this layer for how, and for
     what happens in an engine that cannot do it. */
  .fc-filters {
    margin-top: .5rem;
    border-top: 1px solid var(--fc-border);
  }

  .fc-filters-summary {
    display: flex;
    align-items: center;
    gap: .4rem;
    min-height: var(--fc-tap);
    padding: .35rem .1rem;
    list-style: none;
    cursor: pointer;
    font-size: var(--fc-fs-sm);
    font-weight: 600;
    color: var(--fc-text-muted);
  }
  .fc-filters-summary::-webkit-details-marker { display: none; }

  /* Our own caret, after the label rather than before it — the same
     treatment .fc-nav-summary gets, so every disclosure in the app points
     the same way. */
  .fc-filters-summary::after {
    content: "";
    margin-left: .1rem;
    border: 4px solid transparent;
    border-top-color: currentColor;
    transform: translateY(2px);
  }
  .fc-filters[open] > .fc-filters-summary::after { transform: translateY(-2px) rotate(180deg); }

  /* The active-state read-out. Muted weight, brand colour: it is a status,
     not a control, and must never look tappable on its own. */
  .fc-filters-state {
    font-weight: 500;
    color: var(--fc-link);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .fc-pill-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding: .15rem 0 .5rem;
  }

  @media (min-width: 54rem) {
    /* The bar keeps exactly the spacing it had before the disclosure was
       wrapped around it, so desktop is visually untouched by this pass. */
    .fc-pill-bar { padding: .5rem 0 .35rem; }
  }

  .fc-pill-bar-label {
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fc-text-muted);
  }

  .fc-pill-bar-divider { width: 1px; height: 16px; background: var(--fc-border); margin: 0 6px; align-self: center; }

  /* The input is taken out of the flow, NOT display:none — a hidden input
     is unfocusable, which would make the whole bar keyboard-unreachable.
     This keeps it focusable while the label carries the visuals. */
  .fc-pill-input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }

  .fc-bar-pill {
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    min-height: var(--fc-tap);
    font-size: var(--fc-fs-sm);
    line-height: 1.4;
    padding: 2px 13px;
    border-radius: 9999px;
    border: 1.5px solid var(--fc-border);
    color: var(--fc-text-muted);
    background: var(--fc-surface);
    user-select: none;
    transition: border-color var(--fc-transition), color var(--fc-transition), background var(--fc-transition);
  }
  .fc-bar-pill:hover { border-color: var(--fc-link); color: var(--fc-link); }

  .fc-pill-input:checked + .fc-pill-sort { border-color: var(--fc-link); background: var(--fc-primary-light); color: var(--fc-link); font-weight: 600; }
  .fc-pill-input:checked + .fc-pill-arch { border-color: var(--fc-warn-border); background: var(--fc-warn-bg); color: var(--fc-warn-text); font-weight: 600; }
  .fc-pill-input:checked + .fc-pill-done { border-color: var(--fc-ok-border); background: var(--fc-ok-bg); color: var(--fc-ok-text); font-weight: 600; }
  .fc-pill-input:checked + .fc-pill-pub  { border-color: var(--fc-pub-border); background: var(--fc-pub-bg); color: var(--fc-pub-text); font-weight: 600; }

  /* D-M4-9's placeholder: it must LOOK unavailable, not merely be inert. */
  .fc-pill-input:disabled + .fc-bar-pill-disabled { opacity: .5; cursor: not-allowed; }

  .fc-select {
    font: inherit;
    font-size: var(--fc-fs-sm);
    min-height: var(--fc-tap);
    padding: 2px 8px;
    border: 1.5px solid var(--fc-border);
    border-radius: 9999px;
    background: var(--fc-surface);
    color: var(--fc-text);
  }

  /* Flash / status ------------------------------------------------------- */
  .fc-flash {
    margin: 0 0 1rem;
    padding: .7rem .9rem;
    border-radius: var(--fc-radius);
    border-left: 3px solid var(--fc-border);
    background: var(--fc-surface);
  }
  .fc-flash-saved  { border-left-color: var(--fc-ok-border);   background: var(--fc-ok-bg);   color: var(--fc-ok-text); }
  .fc-flash-notice { border-left-color: var(--fc-warn-border); background: var(--fc-warn-bg); color: var(--fc-warn-text); }

  /* Server-side form errors (auth pages, editor). role="alert" in markup. */
  .error {
    margin: 0 0 1rem;
    padding: .7rem .9rem;
    font-size: var(--fc-fs-md);
    border-radius: var(--fc-radius);
    border-left: 3px solid var(--fc-danger);
    background: var(--fc-danger-bg);
    color: var(--fc-danger-text);
  }

  /* Count + empty states ------------------------------------------------- */
  .fc-count {
    margin: 0 0 .6rem;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
    font-variant-numeric: tabular-nums;
  }

  .fc-empty {
    padding: 1.25rem 1.25rem;
    border: 1px dashed var(--fc-border-strong);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    color: var(--fc-text-muted);
  }
  .fc-empty p { margin: 0 0 .6rem; }
  .fc-empty p:last-child { margin-bottom: 0; }
  .fc-empty-lead { color: var(--fc-text); font-weight: 600; }
  .fc-empty-note { font-size: var(--fc-fs-md); }

  /* Saved public lists panel (M5.4 / D-M5-5) ------------------------------
     Sits below the results and reads as an aside, not a second result list:
     muted, quieter type, no card shadow. It is on every search page — the
     legacy "Show my Public lists" button it replaces was broken for years
     (it fetched a getPublicLists.php that does not exist), so the fix is to
     have nothing to click at all. */
  .fc-saved-lists {
    margin-top: 1.5rem;
    padding-top: 1rem;
    border-top: 1px solid var(--fc-border);
    color: var(--fc-text-muted);
  }
  .fc-saved-lists-title {
    margin: 0 0 .5rem;
    font-size: var(--fc-fs-md);
    font-weight: 600;
    color: var(--fc-text);
  }
  .fc-saved-lists-empty,
  .fc-saved-lists-note { margin: .4rem 0 0; font-size: var(--fc-fs-sm); }
  .fc-saved-lists-items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .4rem .6rem;
  }
  .fc-saved-list-link {
    display: inline-block;
    padding: .2rem .6rem;
    border: 1px solid var(--fc-border);
    border-radius: 9999px;
    background: var(--fc-surface);
    font-size: var(--fc-fs-sm);
  }

  /* The /s/ link in the post-save banner: it is meant to be read and
     copied, so it keeps the monospace figure shapes that make a 64-hex
     code checkable by eye, and wraps rather than overflowing. */
  .fc-flash-link { font-family: var(--fc-font-mono); word-break: break-all; }

  /* Result rows -----------------------------------------------------------
     Density is deliberate: this list is scanned hundreds of times a day
     over ~33k items. Hairline borders, no per-row drop shadow (cheaper
     paint on long lists, calmer to scan), tight vertical rhythm. The due
     ramp keeps its tinted background + 3px left border — the strongest
     signal on the page, as it should be. */
  .fc-item-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .45rem; }

  .fc-item {
    padding: .55rem .85rem;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    transition: border-color var(--fc-transition);
  }
  .fc-item:hover { border-color: var(--fc-border-strong); }

  .fc-item-header { display: flex; align-items: baseline; flex-wrap: wrap; gap: .45rem; }

  .fc-item-type {
    flex: none;
    width: 1.4rem;
    text-align: center;
    color: var(--fc-text-muted);
    font-size: var(--fc-fs-md);
  }

  .fc-item-title { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.35; flex: 1 1 12rem; }

  /* Inline title edit (M4.10) — the #fc-tpl-inline-title clone that
     replaces the heading's contents for the duration of an edit. It
     INHERITS the heading's font so the text does not jump size or weight
     when the field appears and disappears; only the box changes. */
  .fc-inline-title-input {
    width: 100%;
    box-sizing: border-box;
    font: inherit;
    color: inherit;
    padding: .15rem .3rem;
    border: 1px solid var(--fc-accent);
    border-radius: 3px;
    background: var(--fc-surface);
  }

  /* State badges --------------------------------------------------------- */
  .fc-badges { display: inline-flex; flex-wrap: wrap; gap: .3rem; }

  .fc-badge {
    display: inline-block;
    font-size: .7rem;
    font-weight: 500;
    letter-spacing: .02em;
    text-transform: uppercase;
    padding: 1px 7px;
    border-radius: 9999px;
    border: 1px solid var(--fc-border);
    background: var(--fc-tag-bg);
    /* --fc-tag-text, not --fc-text-muted: at badge size on the slate-100
       fill, muted measures 4.45:1 — a hair under AA. */
    color: var(--fc-tag-text);
  }
  .fc-badge-done     { border-color: var(--fc-ok-border);  background: var(--fc-ok-bg);  color: var(--fc-ok-text); }
  .fc-badge-archived { border-color: var(--fc-warn-border); background: var(--fc-warn-bg); color: var(--fc-warn-text); }
  /* Green border/fill carry the "public" identity; the TEXT uses the dark
     ok-green — green-600 text on green-50 is 3.2:1 and unreadable small. */
  .fc-badge-public   { border-color: var(--fc-public-badge-color); background: var(--fc-public-badge-bg); color: var(--fc-ok-text); }
  .fc-badge-slug     { text-transform: none; font-family: var(--fc-font-mono); }

  /* Row actions ----------------------------------------------------------
     DIMMED, NEVER HIDDEN. `opacity: 0` would leave a sighted keyboard user
     tabbing onto a control they cannot see — so the resting state is a
     reduced opacity that is still legible, and BOTH :hover and
     :focus-within bring it to full. The :focus-within half is the one that
     is easy to forget and impossible to notice with a mouse; it is greped
     for by testActionsAreDimmedNotHiddenAndComeBackOnFocusWithin.
     No `display:none` here either: a hidden control is not focusable, and
     the whole point is that Tab reaches it. */
  .fc-item-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem;
    margin-top: .45rem;
    opacity: .6;
    transition: opacity .12s ease-in-out;
  }
  .fc-item:hover .fc-item-actions,
  .fc-item-actions:focus-within { opacity: 1; }

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

  .fc-action {
    display: inline-flex;
    align-items: center;
    min-height: var(--fc-tap);
    font: inherit;
    font-size: var(--fc-fs-sm);
    padding: .1rem .6rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    color: var(--fc-text-muted);
    cursor: pointer;
  }
  .fc-action:hover { border-color: var(--fc-link); color: var(--fc-link); text-decoration: none; }
  .fc-action[disabled] { cursor: default; opacity: .5; }
  .fc-action-danger:hover { border-color: var(--fc-danger); color: var(--fc-danger-text); background: var(--fc-danger-bg); }

  /* Icon controls -------------------------------------------------------
     The row carries FOURTEEN actions where it used to carry four, which is
     only possible because each one is a square glyph rather than a word.
     Borderless at rest so a dense row does not read as a wall of boxes;
     the border and the link colour arrive together on hover and focus,
     which is legacy's `hover:bg-gray-200` said in this design's tokens.

     `min-width` as well as `min-height`: --fc-tap is 28px on a fine
     pointer and 44px on a coarse one, and a control that is tall but
     narrow is not a touch target. */
  .fc-action-icon {
    justify-content: center;
    min-width: var(--fc-tap);
    padding: .1rem;
    border-color: transparent;
    background: transparent;
  }
  .fc-action-icon:hover,
  .fc-action-icon:focus-visible {
    border-color: var(--fc-link);
    color: var(--fc-link);
    background: var(--fc-surface);
  }

  .fc-action-glyph {
    width: 1.15rem;
    height: 1.15rem;
    flex: none;
    pointer-events: none;
    transition: transform .12s ease-in-out;
  }

  /* The expand chevron points down when the body is shut and up when it is
     open — ONE symbol rotated, never two, which is what legacy did too
     (`{rotateOpenCloseIcon180}`). aria-expanded is already the state
     items.js maintains, so the CSS reads it rather than needing a class
     kept in sync with it. */
  .fc-action-icon[aria-expanded="true"] .fc-action-glyph {
    transform: rotate(180deg);
  }

  /* Entries that are one affordance in several controls (open + copy a
     link; QR + open + copy a short link). Grouping them keeps the pair
     adjacent when the row wraps, and gives items.js one element to insert
     and remove as the row's visibility changes. */
  .fc-action-group {
    display: inline-flex;
    align-items: center;
    gap: .15rem;
  }

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

  /* A confirm is a FULL-WIDTH strip in the wrapping action row, not an
     inline pair: appearing mid-row it would shove every icon after it onto
     the next line, moving targets out from under the cursor at the exact
     moment the user is deciding whether to delete something. Taking a line
     of its own, it only makes the row taller. */
  .fc-confirm {
    display: flex;
    flex-basis: 100%;
    gap: .25rem;
    margin-top: .25rem;
  }

  /* The lazily-loaded item body (the expand endpoint's only destination) */
  .fc-item-body {
    margin-top: .55rem;
    padding-top: .55rem;
    border-top: 1px solid var(--fc-border);
    font-size: var(--fc-fs-md);
    overflow-wrap: anywhere;
  }
  .fc-item-body img { max-width: 100%; height: auto; }

  /* Polite live region — empty until something is announced, and taking no
     vertical space while empty, so a quiet row looks quiet. */
  .fc-row-status { margin: .35rem 0 0; font-size: var(--fc-fs-sm); color: var(--fc-danger-text); }
  .fc-row-status:empty { display: none; margin: 0; }

  .fc-item-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .75rem;
    margin: .15rem 0 0;
    font-size: var(--fc-fs-xs);
    color: var(--fc-text-muted);
    font-variant-numeric: tabular-nums;
  }
  .fc-meta-edit { color: var(--fc-link); }

  .fc-tag-list { list-style: none; display: flex; flex-wrap: wrap; gap: .35rem; margin: .4rem 0 0; padding: 0; }

  .fc-tag-chip {
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
    font-size: var(--fc-fs-sm);
    padding: 0 9px;
    border-radius: 9999px;
    background: var(--fc-tag-bg);
    color: var(--fc-tag-text);
  }
  .fc-tag-chip:hover { background: var(--fc-primary-light); color: var(--fc-link); text-decoration: none; }

  /* Due-date urgency ramp — the server picks one of these class names per
     row; no inline colour ever. Class names are frozen legacy. */
  .fc-item.duedatePast  { background: var(--fc-due-past-bg);  border-left: 3px solid var(--fc-due-past-border); }
  .fc-item.duedateToday { background: var(--fc-due-today-bg); border-left: 3px solid var(--fc-due-today-border); }
  .fc-item.duedateNear  { background: var(--fc-due-near-bg);  border-left: 3px solid var(--fc-due-near-border); }
  .fc-item.duedateWeek  { background: var(--fc-due-week-bg);  border-left: 3px solid var(--fc-due-week-border); }
  .fc-item.duedateFar   { background: var(--fc-due-far-bg);   border-left: 3px solid var(--fc-due-far-border); }

  .fc-item.duedatePast  .fc-meta-due { color: var(--fc-due-past-text); font-weight: 600; }
  .fc-item.duedateToday .fc-meta-due { color: var(--fc-due-today-text); font-weight: 600; }
  .fc-item.duedateNear  .fc-meta-due { color: var(--fc-due-near-text); }
  .fc-item.duedateWeek  .fc-meta-due { color: var(--fc-due-week-text); }
  .fc-item.duedateFar   .fc-meta-due { color: var(--fc-due-far-text); }

  /* Pagination -----------------------------------------------------------
     Prev + a five-page window + Next is seven controls, and at the 44px
     coarse-pointer target that is ~330px of row — inside a 375px phone by
     a hair and outside a 320px one. `flex-wrap` on both the row and the
     number list is what keeps that from becoming a horizontally scrolling
     PAGE, which is the one layout failure a phone user cannot work around. */
  .fc-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4rem; margin-top: 1.25rem; }
  .fc-page-list { list-style: none; display: flex; flex-wrap: wrap; justify-content: center; gap: .3rem; margin: 0; padding: 0; }

  .fc-page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--fc-tap);
    min-height: var(--fc-tap);
    padding: .1rem .55rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    color: var(--fc-text-muted);
    font-size: var(--fc-fs-sm);
    font-variant-numeric: tabular-nums;
  }
  .fc-page-link:hover { border-color: var(--fc-link); color: var(--fc-link); text-decoration: none; }

  .fc-page-current { background: var(--fc-primary); border-color: var(--fc-primary); color: var(--fc-on-primary); font-weight: 600; }

  /* Auth pages (login / register / message) ------------------------------ */
  .fc-auth {
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem 1rem;
  }

  .auth-card {
    width: 100%;
    max-width: 22rem;
    padding: 2rem 1.75rem;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    box-shadow: var(--fc-shadow);
  }
  .auth-card h1 { font-size: 1.25rem; margin: 0 0 1.25rem; }

  /* The sign-in lockup. Same mark as the app bar, larger, because a card
     with room is where a brand is allowed to be a brand — and because the
     sign-in page is the one screen where a user needs to be sure which
     application is asking for their password. */
  .fc-auth-brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: 0 0 .9rem;
  }

  .fc-auth-logo {
    display: block;
    height: 2.25rem;
    width: auto;
    padding: 3px 4px;
    border-radius: 7px;
    background: var(--fc-brand-tile);
  }

  .fc-auth-brand-name {
    font-size: var(--fc-fs-sm);
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--fc-link);
  }

  .fc-auth-foot { margin: 1.25rem 0 0; font-size: var(--fc-fs-md); text-align: center; }
  .fc-auth-forgot { margin-top: .75rem; }

  /* Landing (the sign-in page doubles as the front door) ------------------
     Phone: one column, hero → card → features. From 54rem: pitch on the
     left, the card on the right and sticky, so the form never scrolls away
     while a visitor reads. Tokens only — dark mode comes for free. */
  .fc-landing {
    display: block;
    padding: 0;
    background:
      radial-gradient(60rem 30rem at 0% 0%, var(--fc-primary-light), transparent 70%),
      var(--fc-bg);
  }

  .fc-landing-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);   /* grid items default to min-width:auto and overflow a phone */
    gap: 1.5rem;
    max-width: 68rem;
    margin: 0 auto;
    padding: 2rem 1rem 3rem;
  }

  .fc-landing .auth-card { max-width: none; box-shadow: var(--fc-shadow-lg); }

  .fc-landing-title {
    margin: .25rem 0 .75rem;
    font-size: clamp(1.9rem, 6vw, 3rem);
    font-weight: 800;
    line-height: 1.1;
    letter-spacing: -.02em;
  }
  .fc-landing-title span { color: var(--fc-link); }

  .fc-landing-kicker {
    display: inline-block;
    margin: .5rem 0 0;
    padding: .2rem .7rem;
    border-radius: 999px;
    background: var(--fc-danger-bg);
    color: var(--fc-danger-text);
    font-size: var(--fc-fs-sm);
    font-weight: 700;
  }
  .fc-landing-kicker::before { content: "✚ "; color: var(--fc-danger); }

  .fc-landing-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1.4rem; padding: 0; list-style: none; }
  .fc-landing-chips li {
    padding: .2rem .65rem;
    border: 1px solid var(--fc-border-strong);
    border-radius: 999px;
    background: var(--fc-surface);
    font-size: var(--fc-fs-sm);
    font-weight: 600;
    color: var(--fc-text);
  }

  .fc-landing-lead { max-width: 34rem; margin: 0 0 1.25rem; font-size: var(--fc-fs-lg); color: var(--fc-text-muted); }

  .fc-landing-ctas { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.25rem; margin: 0; }
  .fc-landing-cta { padding: .6rem 1.2rem; font-size: 1rem; font-weight: 600; }
  .fc-landing-cta-alt { font-size: var(--fc-fs-md); }
  .fc-landing-ctas-end { margin-top: 1.5rem; }

  .fc-landing-card-title { font-size: 1.25rem; margin: 0 0 1.25rem; }

  .fc-landing-h2 { margin: 2rem 0 1rem; font-size: 1.35rem; font-weight: 700; }
  .fc-landing-features > .fc-landing-h2:first-child { margin-top: .5rem; }
  .fc-landing-cards + .fc-landing-h2 { margin-top: 2.25rem; }

  /* The smart-title demo: the one feature that is easier to show than say. */
  .fc-landing-demo {
    margin: 0 0 1.5rem;
    padding: 1.1rem 1.25rem;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    box-shadow: var(--fc-shadow);
  }
  .fc-landing-demo figcaption,
  .fc-landing-demo-caption { margin: 0 0 .4rem; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }
  .fc-landing-demo-input { margin: 0; }
  .fc-landing-demo-input code {
    display: block;
    padding: .55rem .75rem;
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
    background: var(--fc-bg);
    font-size: var(--fc-fs-md);
    overflow-wrap: anywhere;
  }
  .fc-landing-demo-arrow { margin: .3rem 0; text-align: center; color: var(--fc-text-muted); }
  .fc-landing-demo-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    padding: .55rem .75rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
  }
  .fc-landing-demo-check {
    width: 1rem; height: 1rem;
    border: 2px solid var(--fc-border-strong);
    border-radius: 4px;
  }
  .fc-landing-demo-title { font-weight: 600; margin-right: auto; }
  .fc-landing-demo-tag,
  .fc-landing-demo-due { padding: .1rem .5rem; border-radius: 999px; font-size: var(--fc-fs-xs); font-weight: 600; }
  .fc-landing-demo-tag { background: var(--fc-tag-bg); color: var(--fc-tag-text); }
  .fc-landing-demo-due { background: var(--fc-due-near-bg); color: var(--fc-due-near-text); border: 1px solid var(--fc-due-near-border); }

  .fc-landing-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .fc-landing-cards li {
    padding: 1.1rem 1.15rem;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    transition: border-color var(--fc-transition), transform var(--fc-transition);
  }
  .fc-landing-cards li:hover { border-color: var(--fc-link); transform: translateY(-2px); }
  .fc-landing-cards svg {
    width: 2.25rem; height: 2.25rem;
    padding: .45rem;
    border-radius: var(--fc-radius);
    background: var(--fc-primary-light);
    stroke: var(--fc-link);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    fill: none;
  }
  .fc-landing-cards h3 { margin: .65rem 0 .3rem; font-size: 1rem; }
  .fc-landing-cards p { margin: 0; font-size: var(--fc-fs-md); color: var(--fc-text-muted); }

  .fc-landing-steps { margin: 0; padding: 0; list-style: none; counter-reset: fc-step; display: grid; gap: .75rem; }
  /* Absolute marker, not flex: flex would make every text run, <strong>
     and <kbd> its own column. */
  .fc-landing-steps li { counter-increment: fc-step; position: relative; min-height: 1.75rem; padding: .15rem 0 0 2.6rem; }
  .fc-landing-steps li::before {
    content: counter(fc-step);
    position: absolute;
    left: 0;
    top: 0;
    display: grid;
    place-items: center;
    width: 1.75rem; height: 1.75rem;
    border-radius: 50%;
    background: var(--fc-accent);
    color: var(--fc-on-primary);
    font-weight: 700;
    font-size: var(--fc-fs-sm);
  }

  @media (min-width: 54rem) {
    .fc-landing-grid {
      grid-template-columns: minmax(0, 1fr) 22rem;
      grid-template-areas: "hero card" "features card";
      column-gap: 3rem;
      padding-top: 4rem;
    }
    .fc-landing-hero { grid-area: hero; }
    .fc-landing .auth-card { grid-area: card; align-self: start; position: sticky; top: 2rem; }
    .fc-landing-features { grid-area: features; }
  }

  @media (prefers-reduced-motion: reduce) {
    .fc-landing-cards li { transition: none; }
    .fc-landing-cards li:hover { transform: none; }
  }

  /* Form primitives (auth + editor) -------------------------------------- */
  .fc-field { margin: 0 0 1rem; }

  .fc-label {
    display: block;
    margin: 0 0 .3rem;
    font-size: var(--fc-fs-sm);
    font-weight: 600;
    color: var(--fc-text);
  }

  .fc-input,
  .fc-textarea {
    display: block;
    width: 100%;
    font: inherit;
    min-height: var(--fc-tap);
    padding: .45rem .65rem;
    color: var(--fc-text);
    background: var(--fc-surface);
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
    transition: border-color var(--fc-transition), box-shadow var(--fc-transition);
  }
  .fc-input:focus,
  .fc-textarea:focus {
    outline: none;
    border-color: var(--fc-link);
    box-shadow: 0 0 0 3px var(--fc-primary-ring);
  }

  .fc-input-short { max-width: 8rem; }

  .fc-textarea { resize: vertical; min-height: 12rem; line-height: 1.55; font-family: var(--fc-font-mono); font-size: var(--fc-fs-md); }

  .fc-hint { margin: .3rem 0 0; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }

  /* A checkbox row: real checkbox, label text beside it, whole line
     clickable, target height enforced. */
  .fc-check {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-height: var(--fc-tap);
    margin: 0 0 .5rem;
    cursor: pointer;
    font-size: var(--fc-fs-md);
  }
  .fc-check input { width: 1.05rem; height: 1.05rem; margin: 0; accent-color: var(--fc-primary); }

  .fc-form-actions { display: flex; align-items: center; gap: .6rem; margin-top: 1.5rem; }

  /* ══ Item editor ═══════════════════════════════════════════════════════
     TWO LEVELS OF IMPORTANCE, NOT TEN. What lived here before laid ten
     field groups out as one flat stack with a permanent 50/50 body|preview
     split at the top, so nothing on the page said what the task was.

     `.editor-main` carries the task — title and body, full measure.
     `.editor-rail` carries everything ABOUT it — link, tags, due date,
     priority, visibility, short URL, groups, files. Below 64rem the grid
     is one column and the source order (main, then rail) is already the
     right reading order, so nothing is re-ordered: no `order:`, no JS, and
     the tab order equals the visual order at every width.

     The preview and the three heavy blocks are `<details>`; their
     disclosure behaviour, keyboard operation and expanded-state
     announcement are the platform's. See _editor_form.phtml's header for
     why the dialogs are hoisted out of them. */
  .editor-title { font-size: 1.35rem; margin: .5rem 0 1.25rem; }

  .editor-grid { display: grid; gap: 1.5rem; align-items: start; }
  @media (min-width: 64rem) {
    /* minmax(0,1fr) — not 1fr: a grid item's default min-width is auto, so
       a long unbroken word in the body would otherwise widen the column
       and push the rail off. */
    .editor-grid { grid-template-columns: minmax(0, 1fr) 21rem; gap: 2rem; }
  }

  .editor-main { min-width: 0; }

  /* The one field that should read as the headline of the form. Bigger
     than body text (--fc-fs-lg), heavier, and it is the first thing in the
     tab order. */
  .fc-input-title { font-size: var(--fc-fs-lg); font-weight: 600; padding: .5rem .7rem; }

  /* Tall enough to write in, capped so the rail is still visible on a
     laptop and the field is not taller than a phone screen. */
  .editor-field-body .fc-textarea { min-height: clamp(11rem, 42vh, 28rem); }

  /* Disclosure summary — used by the preview and by every rail section.
     `list-style: none` + the ::-webkit- rule remove the UA triangle; the
     ::before one replaces it, so the caret is ours and points the same way
     in every engine. Rotation is the only motion in the editor and the
     base layer's prefers-reduced-motion rule already flattens it. */
  .editor-summary {
    display: flex;
    align-items: center;
    gap: .45rem;
    min-height: var(--fc-tap);
    padding: .5rem 1rem;
    font-size: var(--fc-fs-xs);
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--fc-text-muted);
    cursor: pointer;
    list-style: none;
    border-radius: var(--fc-radius-lg);
  }
  .editor-summary::-webkit-details-marker { display: none; }
  .editor-summary::before {
    content: "";
    flex: none;
    width: 0;
    height: 0;
    border-left: .34rem solid currentColor;
    border-top: .34rem solid transparent;
    border-bottom: .34rem solid transparent;
    transition: transform var(--fc-transition);
  }
  details[open] > .editor-summary::before { transform: rotate(90deg); }
  .editor-summary:hover { color: var(--fc-text); }

  /* The count beside "Files": the answer to "are there any?" without
     opening anything. */
  .editor-summary-count {
    min-width: 1.35rem;
    padding: 0 .3rem;
    font-size: var(--fc-fs-xs);
    line-height: 1.35rem;
    text-align: center;
    border-radius: 9999px;
    background: var(--fc-tag-bg);
    color: var(--fc-tag-text);
  }

  /* Preview — a disclosure under the body, not a permanent second column.
     Closed by default, and editor.js only fetches a preview while it is
     open, so the ordinary "type and save" path costs zero preview
     requests. */
  .editor-preview { margin: -.25rem 0 0; }
  .editor-preview > .editor-summary { padding-left: 0; padding-right: 0; }

  .preview-pane {
    min-height: 6rem;
    padding: .65rem .8rem;
    font-size: var(--fc-fs-md);
    background: var(--fc-surface);
    border: 1px dashed var(--fc-border-strong);
    border-radius: var(--fc-radius);
    overflow-wrap: anywhere;
  }
  .preview-pane img { max-width: 100%; height: auto; }
  .preview-pane:empty::before { content: "Preview appears as you type."; color: var(--fc-text-muted); font-size: var(--fc-fs-sm); }

  /* ── The rail ──────────────────────────────────────────────────────── */
  .editor-rail { min-width: 0; display: grid; gap: .75rem; align-content: start; }

  .editor-rail-group,
  .editor-section {
    margin: 0;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
  }
  .editor-rail-group { padding: .85rem 1rem; }
  .editor-rail-group > .fc-field:last-child { margin-bottom: 0; }

  .editor-rail-title {
    margin: 0 0 .6rem;
    padding: 0;
    font-size: var(--fc-fs-xs);
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--fc-text-muted);
  }

  /* A real <fieldset>/<legend> for the two flags: they answer one question
     and a screen reader should hear it once, before either answer. The
     legend sits in the border notch like .fc-fieldset's does. */
  .editor-flags { padding: .5rem 1rem .85rem; }
  .editor-flags > .editor-rail-title { padding: 0 .35rem; margin: 0 0 .35rem -.35rem; }
  .editor-flags .fc-check:last-child { margin-bottom: 0; }

  .editor-section-body { padding: 0 1rem .85rem; }
  .editor-section-body > .fc-field:last-child,
  .editor-section-body > .fc-hint:last-child { margin-bottom: 0; }

  /* The single line that replaced two "Save this item first" blocks. */
  .editor-note {
    margin: 0;
    padding: .6rem .8rem;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
    border: 1px dashed var(--fc-border);
    border-radius: var(--fc-radius-lg);
  }

  /* Commit bar. Sticky because the form is taller than the viewport the
     moment an item has attachments, and "Save" being a scroll away from
     the field you are typing in is the friction this pass exists to
     remove. --fc-bg (the page), overridden to --fc-surface in the dialog,
     where the sheet is the surface. */
  .editor-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    margin-top: 1.25rem;
    padding: .75rem 0;
    background: var(--fc-bg);
    border-top: 1px solid var(--fc-border);
  }

  /* The "⌘ or Ctrl + Enter saves" hint beside the commit buttons. Pushed
     to the far end of the bar by an auto margin so the Save/Cancel pair
     keeps its place, and absent below the desktop breakpoint: a phone has
     no modifier key to press and the sticky bar has no room to say so.
     The badge is the same one as .fc-help-keys and .fc-due-keys kbd — see
     the note there on why it is restated rather than shared. */
  .fc-editor-keys { display: none; margin: 0 0 0 auto; align-self: center; }
  @media (min-width: 54rem) {
    .fc-editor-keys { display: block; }
  }
  .fc-editor-keys kbd {
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
  }

  /* Upload area + gallery -------------------------------------------------
     Shared by the editor (upload + remove) and the show page (read-only). */
  /* .upload-area is now a rail <details> (.editor-section carries the
     card); only the file input needs a rule of its own. */
  .upload-area input[type="file"] { max-width: 100%; font-size: var(--fc-fs-sm); }
  .upload-hint { margin: .4rem 0 0; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }
  .upload-hint:empty { margin: 0; }

  /* Dropzone. The drop target used to be the .upload-area itself, which
     once Files moved inside a <details> meant a ~30px summary strip on any
     item with no files yet — hittable in principle, not in practice. This
     is a real target with a visible edge, and it is inside the section so
     it only exists when the section is open.

     The dashed edge is the affordance: a solid box reads as a container,
     a dashed one reads as somewhere to put things. */
  .upload-dropzone {
    margin: .5rem 0 0;
    padding: .75rem;
    border: 1px dashed var(--fc-border-strong);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    transition: border-color var(--fc-transition), background-color var(--fc-transition);
  }

  /* Set by uploader.js only for a drag that actually carries files. The
     drop target is the WHOLE FORM (see uploader.js's dropTarget), so both
     light up: the dashed ring on the form says "anywhere here works", the
     box brightening says where the file ends up. The ring is drawn inward
     (negative offset) so a modal's scroll box cannot clip it. */
  .editor-form.is-dragover {
    outline: 2px dashed var(--fc-link);
    outline-offset: -2px;
    border-radius: var(--fc-radius-lg);
  }

  .upload-dropzone.is-dragover {
    border-color: var(--fc-link);
    border-style: solid;
    background: var(--fc-primary-ring);
  }

  .upload-dropzone-hint {
    margin: .35rem 0 0;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
  }

  /* Queued-file list — a NEW item only, where the files wait for the save
     that gives them an item to belong to. Deliberately not styled as the
     upload gallery: these are not attachments yet, and a list that looked
     identical to one would say they were. */
  .upload-pending {
    margin: .5rem 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: .25rem;
  }

  .upload-pending-item {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3rem .5rem;
    font-size: var(--fc-fs-sm);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
  }

  .upload-pending-item .upload-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

  .upload-pending-note {
    flex: 0 0 auto;
    color: var(--fc-text-muted);
    font-size: var(--fc-fs-sm);
  }

  .upload-pending-remove {
    flex: 0 0 auto;
    font: inherit;
    color: var(--fc-text);
    background: none;
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius-lg);
    padding: .2rem .5rem;
    cursor: pointer;
  }

  .upload-gallery,
  .item-attachments {
    list-style: none;
    margin: .6rem 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    gap: .6rem;
  }
  .upload-gallery:empty, .item-attachments:empty { margin: 0; }

  .upload-item,
  .item-attachment {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    padding: .5rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    font-size: var(--fc-fs-sm);
    min-width: 0;
  }
  .upload-thumb,
  .attachment-thumb {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: 4px;
    background: var(--fc-tag-bg);
  }
  .upload-name { overflow-wrap: anywhere; color: var(--fc-text); }
  .upload-item a, .item-attachment a { overflow-wrap: anywhere; }

  .upload-delete {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    min-height: var(--fc-tap);
    font: inherit;
    font-size: var(--fc-fs-sm);
    padding: .1rem .6rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    color: var(--fc-text-muted);
    cursor: pointer;
  }
  .upload-delete:hover { border-color: var(--fc-danger); color: var(--fc-danger-text); background: var(--fc-danger-bg); }

  /* Item show ------------------------------------------------------------- */
  .item-show .item-title { font-size: 1.5rem; font-weight: 700; margin: .5rem 0 .75rem; }

  .item-show .item-url { margin: 0 0 .5rem; font-size: var(--fc-fs-md); overflow-wrap: anywhere; }

  .item-show .item-due-date {
    display: inline-block;
    margin: 0 0 .75rem;
    padding: .15rem .6rem;
    font-size: var(--fc-fs-sm);
    border-radius: 9999px;
    background: var(--fc-tag-bg);
    color: var(--fc-tag-text);
  }

  .item-body {
    padding: 1rem 0;
    border-top: 1px solid var(--fc-border);
    line-height: 1.65;
    overflow-wrap: anywhere;
  }
  .item-body img { max-width: 100%; height: auto; }
  .item-body pre { overflow-x: auto; padding: .6rem .8rem; background: var(--fc-tag-bg); border-radius: var(--fc-radius); }
  .item-body blockquote { margin: 0 0 1em; padding: .1rem 0 .1rem .9rem; border-left: 3px solid var(--fc-border-strong); color: var(--fc-text-muted); }

  .item-show-nav { display: flex; gap: .6rem; margin: 1rem 0 0; padding-top: .75rem; border-top: 1px solid var(--fc-border); }

  /* Short URL fieldset + QR dialog (task M5.9) ---------------------------- */
  /* Additive only: every colour below is an existing semantic token, so the
     fieldset inherits the design system rather than restating it. The QR
     IMAGE itself is deliberately NOT styled from here — it arrives through
     <img src="/api/qr...">, and CSS does not cascade into an <img>, which is
     exactly why M5.8's renderer carries its own presentation attributes. */
  .fc-fieldset {
    margin: 1rem 0 0;
    padding: .75rem 1rem 1rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
  }
  .fc-fieldset > legend { padding: 0 .35rem; font-size: var(--fc-fs-sm); font-weight: 600; color: var(--fc-text-muted); }

  .short-url-link { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; overflow-wrap: anywhere; }

  /* Three states, matching statusClass() in shorturl.js: no class = nothing
     said yet (muted), is-available, is-taken. The neutral state matters —
     a field that keeps a stale "available" while the user types is how a
     wrong verdict ends up next to a slug someone else already holds. */
  .short-url-status { min-height: 1.2em; }
  .short-url-status.is-available { color: var(--fc-ok-text); }
  .short-url-status.is-taken { color: var(--fc-danger-text); }

  .fc-btn-small { padding: .2rem .55rem; font-size: var(--fc-fs-sm); }

  .fc-dialog {
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
    padding: 1rem 1.25rem;
    background: var(--fc-surface);
    color: var(--fc-text);
    max-width: min(92vw, 22rem);
  }
  .fc-dialog::backdrop { background: rgb(15 23 42 / .45); }
  .fc-dialog-title { margin: 0 0 .75rem; font-size: var(--fc-fs-md); font-weight: 600; }
  .qr-dialog img { display: block; width: 100%; height: auto; max-width: 16rem; margin: 0 auto .5rem; }

  /* ══ The editor dialog (item-modal.js) ═════════════════════════════════
     THE SAME .fc-dialog AS EVERY OTHER DIALOG IN THIS APP, widened. It is
     a native <dialog> opened with showModal(), so the backdrop, the
     focus trap, inertness of the page behind it and Escape all come from
     the platform; item-modal.js only adds what the platform cannot know
     (which URL to load, the unsaved-changes guard, and returning focus).

     THREE BANDS, one shape each:
       ≥ 46rem   a centred sheet, capped at 68rem and at 92dvh.
       < 46rem   full-screen. A 22rem-wide dialog holding a two-column
                 form on a 375px phone is not a dialog, it is a keyhole;
                 at that width the editor IS the screen, which is also
                 what the standalone page would have given.
     dvh, not vh: on iOS the URL bar makes 100vh taller than the visible
     viewport, which puts the sticky action bar under the browser chrome —
     the one place a Save button must never be.

     ═══ `[open]` IS LOAD-BEARING ══════════════════════════════════════════
     This rule sets `display`, and an author `display` on a <dialog> beats
     the UA sheet's `dialog:not([open]) { display: none }` outright — author
     styles win over user-agent styles whatever the specificity or layer. It
     shipped unscoped and put a bordered "Opening the editor…" box at the
     foot of every search page, unnoticed for months because it sat below the
     results; the item page began mounting the same dialog above the fold and
     it became impossible to miss. SearchPagesTest::
     testNoDialogRuleSetsDisplayWithoutScopingItToOpen greps for the whole
     class of mistake now, over every dialog class the templates declare. */
  .editor-dialog[open] {
    width: min(96vw, 68rem);
    max-width: none;
    max-height: min(92dvh, 60rem);
    padding: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  /* The scrollport. The dialog itself must not scroll, or the close button
     would scroll away with the content. */
  .editor-dialog-body {
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: 1rem 1.25rem 1.25rem;
  }

  /* A long rich-text body scrolls inside its own pane, not the dialog body,
     so the formatting toolbar above it stays in view. Both panes share the
     cap so the HTML toggle is still a content swap, not a layout jump. */
  .editor-dialog .rich-editor-visual,
  .editor-dialog .rich-editor-source {
    max-height: min(50dvh, 28rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .editor-dialog .editor-actions { background: var(--fc-surface); }
  .editor-dialog .editor-title { margin: 0 2.5rem 1rem 0; font-size: 1.15rem; }

  /* Top-right, over the scrollport rather than inside it. A second way
     out beside Escape and Cancel — the one a thumb reaches on a phone. */
  .editor-dialog-close {
    position: absolute;
    top: .5rem;
    right: .5rem;
    z-index: 6;
  }
  .editor-dialog-close svg { width: 1.1rem; height: 1.1rem; }

  /* Visible while the fragment is in flight. role="status" in markup, so
     it is announced rather than merely drawn. */
  .editor-dialog-loading {
    margin: 0;
    padding: 2.5rem 1.25rem;
    text-align: center;
    color: var(--fc-text-muted);
    font-size: var(--fc-fs-md);
  }

  /* The unsaved-changes prompt. The LAST flex child of the dialog, so it
     sits under the scrollport as a strip rather than covering the work it
     is asking about — the same judgement the row's delete confirm records
     ("a full-width strip, not an inline pair"). Danger tokens because
     "Discard changes" is the destructive answer; "Keep editing" is the
     primary and takes focus. */
  .editor-dialog-guard {
    flex: none;
    padding: .8rem 1.25rem 1rem;
    border-top: 1px solid var(--fc-danger);
    background: var(--fc-danger-bg);
    color: var(--fc-danger-text);
  }
  .editor-dialog-guard-text { margin: 0; font-size: var(--fc-fs-md); }
  .editor-dialog-guard .fc-form-actions { margin-top: .6rem; }

  @media (max-width: 46rem) {
    /* `[open]` for the same reason as above — and so the two blocks stay one
       selector, rather than one that matches a closed dialog and one that
       does not. */
    .editor-dialog[open] {
      width: 100vw;
      max-width: 100vw;
      height: 100dvh;
      max-height: 100dvh;
      margin: 0;
      border: 0;
      border-radius: 0;
    }
    /* One column on a phone, and the rail sections stay collapsed until
       asked for — the form is then a title, a body and a short list of
       named things, which is the whole point of the rail being <details>. */
    .editor-dialog-body { padding: 1rem .9rem 1.25rem; }
  }
}

/* ============================================================================
   M6.8 — GROUPS. Additive section: the /groups page and the invitation page.
   ============================================================================

   WHY IT IS A SEPARATE @layer BLOCK AT THE END OF THE FILE. Two M6 tasks add
   rules to this stylesheet — M6.8 (this one) and M6.10 (search-row chips, the
   read-only badge, the share dialog) — and they run in the same wave. A layer
   block may be reopened as often as you like, so appending one costs nothing
   and gives each task a contiguous region instead of two lanes editing the
   same lines. @layer components was already open above; reopening it here
   keeps these rules at the same cascade precedence as every other component.

   ► M6.10 APPENDS BELOW THIS BLOCK. Do not insert inside it, and do not
   redefine .fc-group-badge — see its rule for why it lives here.

   These rules are also the home of a handful of GENERIC pieces the design
   system happened not to need until now (.fc-btn-danger, .fc-section,
   .fc-page-head, .fc-field-label, .fc-form-status, .fc-shell-narrow). They
   are named generically on purpose; if a later pass hoists them into the
   components block above, delete them here rather than leaving two copies.

   No colour literals: everything speaks in the tokens declared at the top of
   this file, so dark mode needs nothing added here.
   ========================================================================== */

@layer components {

  /* ── shared scaffolding ─────────────────────────────────────────────── */

  .fc-shell-narrow { max-width: 34rem; }

  .fc-page-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 0 0 1.25rem;
  }
  .fc-page-title { margin: 0; font-size: 1.35rem; font-weight: 650; }

  .fc-section { margin: 0 0 2rem; }
  .fc-section:last-child { margin-bottom: 0; }
  .fc-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
  }
  .fc-section-title {
    margin: 0 0 .6rem;
    font-size: var(--fc-fs-md);
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fc-text-muted);
  }
  .fc-section-head .fc-section-title { margin-bottom: 0; }

  .fc-field-label { display: block; margin: 0 0 .25rem; font-size: var(--fc-fs-sm); font-weight: 600; }
  .fc-form-status { margin: .5rem 0 0; min-height: 1.2em; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }
  .fc-input-small { padding: .2rem .4rem; font-size: var(--fc-fs-sm); }

  /* Destructive actions. They are red BEFORE they are armed too — a button
     that only looks dangerous once you have already pressed it is a button
     that surprises people. The armed state adds weight, and it is a class,
     never an inline style: groups.js sets .is-armed, and the CSP scan on
     rendered HTML could not see a JS-assigned style at all. */
  .fc-btn-danger { color: var(--fc-danger-text); border-color: var(--fc-danger); }
  .fc-btn-danger:hover { background: var(--fc-danger-bg); color: var(--fc-danger-text); }
  .fc-btn-danger.is-armed {
    background: var(--fc-danger);
    border-color: var(--fc-danger);
    color: var(--fc-on-primary);
    font-weight: 650;
  }

  /* ── pending invitations, on /groups ────────────────────────────────── */

  /* Accent-bordered and first on the page (UX §2.4): an invitation is the
     only thing here that expires, so it is the only thing that gets to
     interrupt. */
  .fc-invitation-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
  .fc-invitation-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: .7rem .9rem;
    border: 1px solid var(--fc-border);
    border-left: 3px solid var(--fc-accent);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
  }
  .fc-invitation-group { font-weight: 650; }

  /* ── group cards ────────────────────────────────────────────────────── */

  .fc-group-list { list-style: none; margin: .75rem 0 0; padding: 0; display: grid; gap: 1rem; }
  .fc-group-card {
    padding: .9rem 1rem 1rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
  }
  .fc-group-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    flex-wrap: wrap;
  }
  .fc-group-name { margin: 0; font-size: var(--fc-fs-md); font-weight: 650; }
  .fc-group-desc { margin: .35rem 0 0; font-size: var(--fc-fs-md); color: var(--fc-text-muted); }
  .fc-badge-role { text-transform: none; }

  /* The member table is deliberately dense and quiet: it is a working list,
     not a directory. It scrolls INSIDE the card rather than widening it —
     a long username must never push the page into a horizontal scroll. */
  .fc-member-table {
    width: 100%;
    margin: .8rem 0 0;
    border-collapse: collapse;
    font-size: var(--fc-fs-sm);
  }
  .fc-member-table th,
  .fc-member-table td {
    padding: .3rem .4rem;
    text-align: left;
    border-bottom: 1px solid var(--fc-border);
    vertical-align: middle;
  }
  .fc-member-table th {
    font-size: var(--fc-fs-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fc-text-muted);
    font-weight: 600;
  }
  .fc-member-table tr:last-child td { border-bottom: 0; }
  .fc-member-name { overflow-wrap: anywhere; }
  .fc-member-role-text { color: var(--fc-text-muted); }
  .fc-member-actions { text-align: right; white-space: nowrap; }

  .fc-invite-row {
    display: flex;
    align-items: flex-end;
    gap: .6rem;
    flex-wrap: wrap;
    margin: .9rem 0 0;
  }
  .fc-invite-row .fc-field { margin: 0; flex: 1 1 14rem; }
  .fc-field-inline { margin: 0; }

  .fc-pending-list { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .3rem; }
  .fc-pending-invitation {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    font-size: var(--fc-fs-sm);
  }
  .fc-pending-email { overflow-wrap: anywhere; }
  .fc-pending-note {
    font-size: var(--fc-fs-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fc-text-muted);
  }

  .fc-group-danger {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    margin: 1rem 0 0;
    padding: .6rem 0 0;
    border-top: 1px solid var(--fc-border);
  }
  .fc-group-danger .fc-hint { margin: 0; }
  .fc-group-status { margin: .5rem 0 0; min-height: 1.2em; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }

  /* ── shared with me ─────────────────────────────────────────────────── */

  .fc-shared-table { width: 100%; border-collapse: collapse; font-size: var(--fc-fs-sm); }
  .fc-shared-table th,
  .fc-shared-table td {
    padding: .35rem .4rem;
    text-align: left;
    border-bottom: 1px solid var(--fc-border);
  }
  .fc-shared-table th {
    font-size: var(--fc-fs-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--fc-text-muted);
    font-weight: 600;
  }
  .fc-shared-title { overflow-wrap: anywhere; }
  .fc-shared-owner,
  .fc-shared-updated { color: var(--fc-text-muted); white-space: nowrap; }

  /* THE ONE CLASS M6.10 ALSO NEEDS, defined once, here. It is the group chip:
     this table uses it for the group column, and M6.10's search rows use it
     for the per-row chips. Both link to /search?ricerca=@group:{token}, both
     must look identical, and two definitions of one chip in one stylesheet
     would drift. M6.10: reuse it, do not redefine it. */
  .fc-group-badge {
    display: inline-block;
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border-strong);
    border-radius: 999px;
    font-size: var(--fc-fs-xs);
    line-height: 1.5;
    color: var(--fc-link);
    text-decoration: none;
    overflow-wrap: anywhere;
  }
  .fc-group-badge:hover { background: var(--fc-primary-light); text-decoration: none; }

  /* ── the invitation page ────────────────────────────────────────────── */

  /* Deliberately roomier than the rest of the app. The invitation is an
     entry point — often the first v2 page a new member ever sees — and UX
     appendix §2.4 asks it to feel like a front door rather than a row in a
     dense working tool. */
  .fc-invitation {
    padding: 1.5rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
  }
  .fc-invitation-title { margin: 0 0 .75rem; font-size: 1.25rem; font-weight: 650; }
  .fc-invitation-lead { margin: 0 0 .75rem; }
  .fc-invitation-note { margin: 0 0 1.25rem; font-size: var(--fc-fs-md); color: var(--fc-text-muted); }
  .fc-invitation-form { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
  .fc-invitation-form .fc-form-status { flex-basis: 100%; margin-top: 0; }
  .fc-invitation-later { margin: 1.25rem 0 0; font-size: var(--fc-fs-sm); }

  /* Every terminal state (expired, declined, cancelled, unknown, and the
     neutral not-for-this-account one) is quiet by design: nothing here is
     an error the reader can act on, so nothing shouts. */
  .fc-invitation[data-invitation-state="unknown"] .fc-invitation-title,
  .fc-invitation[data-invitation-state="not-invitee"] .fc-invitation-title,
  .fc-invitation[data-invitation-state="expired"] .fc-invitation-title,
  .fc-invitation[data-invitation-state="cancelled"] .fc-invitation-title,
  .fc-invitation[data-invitation-state="declined"] .fc-invitation-title {
    color: var(--fc-text-muted);
  }

  .fc-dialog-actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: 1rem; }
}

/* ============================================================================
   M6.10 — GROUPS ON THE SEARCH ROW AND IN THE EDITOR.
   ============================================================================

   Appended BELOW M6.8's block, as that block's own marker asks. Reopening
   @layer components once more costs nothing and keeps these rules at the
   same cascade precedence as everything above.

   .fc-group-badge IS NOT REDEFINED HERE. It is the chip, it is defined once
   in M6.8's block, and the search-row chips below simply wear it — the two
   surfaces link to the same `/search?ricerca=@group:{token}` and must look
   identical. SearchPagesTest asserts there is exactly one definition of it
   in this file.

   TWO CLASSES THAT WERE USED BEFORE THEY EXISTED. `.fc-badge-readonly`
   (templates/items/show.phtml, M6.7) and `.fc-badge-editor` (show.phtml AND
   items/editor.phtml, M6.11) were both rendered by shipped templates and
   styled nowhere, so they inherited the bare `.fc-badge` shape and were
   indistinguishable from a "Done" chip. Closed here, in the block that owns
   the group vocabulary, rather than up in the badge run — if a later pass
   hoists them, delete them here rather than leaving two copies.

   No colour literals: tokens only, so dark mode needs nothing added.
   ========================================================================== */

@layer components {

  /* ── the two missing badges ─────────────────────────────────────────── */

  /* Quiet on purpose. "Read-only" is not a warning and not an error — it is
     a statement of what this row is, and the row's own content is what the
     reader came for. It reads as muted, not as a problem. */
  .fc-badge-readonly {
    border-color: var(--fc-border-strong);
    background: transparent;
    color: var(--fc-text-muted);
  }

  /* The editor badge says "you may change this", so it carries the primary
     accent — the same signal the app's other affirmative affordances use. */
  .fc-badge-editor {
    border-color: var(--fc-primary);
    background: var(--fc-primary-light);
    color: var(--fc-text);
  }

  /* ── row chips ──────────────────────────────────────────────────────── */

  /* Same list geometry as .fc-tag-list, so a row carrying both reads as one
     band of chips rather than two competing rows of furniture. */
  .fc-group-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin: .15rem 0 0;
    padding: 0;
    list-style: none;
  }

  .fc-meta-owner { color: var(--fc-text-muted); }
  .fc-owner-name { color: var(--fc-text); }

  /* ── the share dialog ───────────────────────────────────────────────── */

  .share-dialog { max-width: min(92vw, 26rem); }

  .fc-group-choices {
    margin: .25rem 0 0;
    padding: 0;
    list-style: none;
    max-height: 50vh;
    overflow-y: auto;
  }

  .fc-group-choice {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .6rem;
    padding: .3rem 0;
    border-bottom: 1px solid var(--fc-border);
  }
  .fc-group-choice:last-child { border-bottom: 0; }

  /* A group the caller may not share into is still listed (see
     GroupApiController::itemGroups()); dimming says "not yours to change"
     without hiding a share the user can see everywhere else. */
  .fc-group-choice input:disabled + .fc-group-choice-name { color: var(--fc-text-muted); }

  .fc-group-choice-status {
    font-size: var(--fc-fs-xs);
    color: var(--fc-text-muted);
    text-align: right;
  }

  /* ── the editor's Groups block ──────────────────────────────────────── */

  .fc-groups-fieldset .fc-group-choices { max-height: none; }
  .fc-groups-hint { color: var(--fc-text-muted); }
}

/* ==========================================================================
   TASK M7.10 — /tools (the card index) and /tools/notetaker.

   Appended, in @layer components once more, for the same reason every block
   above says so: same cascade precedence as the rest of the design system.

   Nothing new is invented here. A tool card is the group card's geometry
   (.fc-group-card) under its own name, because the two are the same object
   — a bordered panel in a one-column grid — and giving the tools page its
   own class means a later change to group cards cannot silently restyle a
   page nobody was thinking about.

   No colour literals: tokens only, so dark mode needs nothing added.
   ========================================================================== */

@layer components {

  .fc-tool-list { list-style: none; margin: .75rem 0 0; padding: 0; display: grid; gap: 1rem; }

  .fc-tool-card {
    padding: .9rem 1rem 1rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
  }

  .fc-tool-name { margin: 0; font-size: var(--fc-fs-md); font-weight: 650; }
  .fc-tool-desc { margin: .35rem 0 0; font-size: var(--fc-fs-md); color: var(--fc-text-muted); }
  .fc-tool-actions { margin: .7rem 0 0; }

  /* Quieter than the description: these lines are caveats ("Admins only",
     "clicking it here does nothing"), and a caveat that shouts reads as an
     error. */
  .fc-tool-note { margin: .5rem 0 0; font-size: var(--fc-fs-sm); color: var(--fc-text-muted); }

  /* ── the notetaker editor ───────────────────────────────────────────── */

  /* Monospace and tab-size 4: the note is TaskDown, whose entire structure
     is carried by leading tabs. A proportional font with the browser's
     default 8-wide tab makes a three-level outline unreadable — the one
     styling decision on this page that is about the content rather than
     about the chrome. */
  .fc-notetaker-editor {
    width: 100%;
    min-height: 60vh;
    margin: .25rem 0 0;
    padding: .6rem .7rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    color: var(--fc-text);
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-md);
    line-height: 1.5;
    tab-size: 4;
    resize: vertical;
  }
}

/* ==========================================================================
   GLOBAL NAVIGATION — THE APP BAR (templates/layout/_nav.phtml)

   In @layer components once more, for the same reason every block above
   says so: same cascade precedence as the rest of the design system. It
   shipped UNLAYERED once, which is worse than inconsistent — unlayered
   styles WIN over layered ones, so no rule in this stylesheet could ever
   have overridden a nav rule. NavPartialTest asserts the layer.

   Built on the existing --fc-* tokens only; the one new colour token,
   --fc-brand-tile, is declared in :root with the others and explains
   itself there.

   ─── WHAT THIS REPLACED, AND WHY ───────────────────────────────────────
   The previous version was one wrapping row of nine equal-weight pills.
   On a 375px phone that is three rows of grey — about 140px — of
   undifferentiated links above a sticky search header that was itself
   taking ~350px. Roughly 490px of a 667px screen was chrome before the
   first result, and none of it had a hierarchy: "Contacts" and "Admin"
   and "Log out" all looked like the same kind of thing, and there was no
   brand mark, so the top of the page had no fixed point to read from.

   The row now becomes an APP BAR: a brand lockup on the left, and below
   the breakpoint one disclosure on the right that opens the whole menu as
   a drawer. 52px, fixed, sticky. Everything the old row showed is still
   there; it is one tap away instead of three rows tall.

   ─── THE ROW STILL WRAPS, AND IT STILL MUST NEVER SCROLL ───────────────
   Kept verbatim from the previous banner, because the trap has not moved:
   `.fc-nav-list` shipped as `overflow-x: auto`, meant as a mobile scroll
   strip. Per CSS Overflow Level 3, an axis specified `visible` computes to
   `auto` when the OTHER axis is `auto` — so the list quietly became a
   scroll container in BOTH directions, which put `More` and `Admin` (and
   with them Tools, the bookmarklet and the only way to log out) off the
   right edge, and clipped any dropdown that opened inside it.
   `flex-wrap: wrap` costs a second row on a narrow screen and removes both
   failures at the source. Do not reintroduce overflow on `.fc-nav-list` to
   "tidy" the wrap: NavPartialTest fails on the property's presence.
   (The DRAWER PANEL around the list may scroll — see .fc-nav-panel — and
   that is safe only because the dropdowns inside it are static below the
   breakpoint rather than absolutely positioned.)

   ─── STICKY BELOW THE BREAKPOINT ONLY ──────────────────────────────────
   The bar is sticky on a phone, where a menu you have to scroll to the top
   of a 500-row list to reach is a menu you do not use, and where its
   height is guaranteed (brand + one toggle, always one line). Above 54rem
   the entry row can wrap to two lines and the height stops being
   knowable — and `.fc-header` sticks at exactly --fc-appbar-h — so the bar
   goes back to scrolling with the page there, exactly as it always did.
   ========================================================================== */

@layer components {

  .fc-nav {
    position: sticky;
    top: 0;
    /* Above .fc-header (10) and above .fc-omnibox-list (20): the app bar
       and its drawer are the topmost page chrome, and a suggestion popup
       drawn over the menu that spawned it reads as a rendering fault. */
    z-index: 50;
    background: var(--fc-surface);
    border-bottom: 1px solid var(--fc-border);
  }

  /* The bar itself. FULL-BLEED, not boxed to --fc-column: it is a direct
     child of <body> on every page, and an app bar that stops short of the
     viewport edge reads as a card floating on the page rather than as the
     top of the application. It keeps the shell's own gutter as its inner
     inset, so on a phone — where the shell is full-width too — the brand
     sits exactly above the content it belongs to. */
  .fc-nav-bar {
    display: flex;
    align-items: center;
    gap: .75rem;
    min-height: var(--fc-appbar-h);
    padding: 0 var(--fc-gutter);
  }

  /* ── brand ──────────────────────────────────────────────────────────
     Home lives at the top-left corner of a page because that is where
     everyone has looked for thirty years (Fitts: it is also a corner, so
     it is an infinitely large target). It doubles as the one fixed point
     that tells you which application you are in. */
  .fc-nav-brand {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    flex: none;
    min-height: var(--fc-tap);
    padding: .25rem;
    margin-left: -.25rem;
    border-radius: var(--fc-radius);
    color: var(--fc-text);
    text-decoration: none;
  }
  .fc-nav-brand:hover { color: var(--fc-text); text-decoration: none; }

  /* The padding is what the DARK bar sees: the artwork's own white ground
     becomes a chip, and without a little air the petals touch its corners
     and read as a cropped sticker. On the light bar the chip is the same
     colour as the surface, so this costs nothing there. `box-sizing:
     border-box` (base layer) keeps the row height at exactly 1.75rem
     either way. */
  .fc-nav-logo {
    display: block;
    height: 1.75rem;
    width: auto;
    padding: 2px 3px;
    border-radius: 5px;
    background: var(--fc-brand-tile);
  }

  .fc-nav-wordmark {
    font-size: var(--fc-fs-md);
    font-weight: 700;
    letter-spacing: -.01em;
  }

  /* ── the drawer ─────────────────────────────────────────────────────
     A native <details>, for exactly the reasons layout/_nav.phtml's
     docblock gives for the dropdowns inside it: it opens with a keyboard,
     it is announced as a disclosure with its expanded state, and it works
     with JavaScript switched off — which under a CSP with no
     'unsafe-inline' is not a nicety, it is the only shape that works at
     all without a second external module. */
  .fc-nav-drawer {
    margin-left: auto;
    min-width: 0;
  }

  .fc-nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .45rem;
    min-height: var(--fc-tap);
    min-width: var(--fc-tap);
    padding: .35rem .7rem;
    /* --fc-border-strong, the same edge .fc-btn carries: this is the only
       control on the phone's app bar, and giving it a fainter boundary
       than every other button in the application would make the one thing
       a new user has to find the hardest one to see. */
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    color: var(--fc-text);
    font-size: var(--fc-fs-md);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    user-select: none;
  }
  .fc-nav-toggle::-webkit-details-marker { display: none; }
  .fc-nav-toggle:hover { border-color: var(--fc-border-strong); background: var(--fc-primary-light); }
  .fc-nav-drawer[open] > .fc-nav-toggle {
    border-color: var(--fc-primary);
    background: var(--fc-primary-light);
    color: var(--fc-link);
  }

  /* The three bars, drawn rather than downloaded: two pseudo-elements and
     the box's own border, so the icon costs zero bytes and zero requests
     and inherits the button's colour in both schemes. Open, the outer two
     rotate into a cross and the middle one goes — the state change is
     visible without relying on the word beside it. */
  .fc-nav-toggle-bars {
    position: relative;
    display: block;
    width: 1rem;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transition: background var(--fc-transition);
  }
  .fc-nav-toggle-bars::before,
  .fc-nav-toggle-bars::after {
    content: "";
    position: absolute;
    left: 0;
    width: 1rem;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transition: transform var(--fc-transition);
  }
  .fc-nav-toggle-bars::before { top: -5px; }
  .fc-nav-toggle-bars::after  { top:  5px; }

  .fc-nav-drawer[open] .fc-nav-toggle-bars { background: transparent; }
  .fc-nav-drawer[open] .fc-nav-toggle-bars::before { transform: translateY(5px) rotate(45deg); }
  .fc-nav-drawer[open] .fc-nav-toggle-bars::after  { transform: translateY(-5px) rotate(-45deg); }

  /* The panel OVERLAYS the page instead of pushing it down: a menu that
     reflows the document shifts the result the user was reading, and on a
     phone that is the whole visible list. Anchored to .fc-nav, which is
     the sticky (therefore positioned) ancestor.

     It may scroll — nine entries with a group open exceed a short phone —
     and that is only safe because .fc-nav-menu is STATIC down here. Make
     the sub-menus absolute again at this width and the scroll container
     will clip them, which is the exact bug the banner above records. */
  .fc-nav-panel {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    max-height: calc(100dvh - var(--fc-appbar-h));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: .4rem var(--fc-gutter) .75rem;
    background: var(--fc-surface);
    border-bottom: 1px solid var(--fc-border);
    box-shadow: var(--fc-shadow-lg);
  }

  /* ── the entry list ─────────────────────────────────────────────────
     A COLUMN on a phone: nine destinations read as a list, not as a bag of
     pills, and a full-width row is a 44px-tall target that spans the
     screen rather than a 60px one you have to aim at.
     `flex-wrap: wrap` stays — it is what the banner above is about, and
     NavPartialTest asserts it. There is deliberately NO `overflow` here. */
  .fc-nav-list {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .fc-nav-item {
    position: relative;
    flex: 0 0 auto;
  }

  /* The Log out entry is a <form> (POST-only route). `display: contents`
     makes the wrapper vanish from layout so its button sits in the row
     exactly where an anchor would. */
  .fc-nav-form { display: contents; }

  /* button.fc-nav-link must not inherit the UA's button chrome — same role
     in the menu, same look as the anchors beside it. */
  button.fc-nav-link {
    background: none;
    border: 0;
    font: inherit;
    text-align: left;
  }

  .fc-nav-link,
  .fc-nav-summary {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: var(--fc-tap);
    padding: 8px 12px;
    border-radius: var(--fc-radius);
    color: var(--fc-text-muted);
    font-size: var(--fc-fs-md);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
  }

  .fc-nav-link:hover,
  .fc-nav-summary:hover {
    background: var(--fc-primary-light);
    color: var(--fc-link-hover);
  }

  /* Never remove the focus ring — a nav is the first thing a keyboard user
     lands on, and an invisible focus makes the whole page unnavigable. */
  .fc-nav-link:focus-visible,
  .fc-nav-summary:focus-visible,
  .fc-nav-toggle:focus-visible,
  .fc-nav-brand:focus-visible {
    outline: 2px solid var(--fc-primary);
    outline-offset: 2px;
  }

  /* "You are here". The inset underline is a DESKTOP tab metaphor and
     reads as a stray hairline in a vertical list, so down here the current
     entry carries a left bar and a tinted ground instead — the same shape
     the item rows use for their due-date band, which is the one visual
     idiom this application already has for "this row is special".
     aria-current="page" carries it to anyone not looking at the colour. */
  .fc-nav-current {
    color: var(--fc-link);
    font-weight: 700;
    background: var(--fc-primary-light);
    box-shadow: inset 3px 0 0 var(--fc-primary);
    cursor: default;
  }

  /* Suppress the UA disclosure triangle and supply our own, so the caret
     sits AFTER the label rather than before it. Both forms are needed:
     `list-style` covers Firefox, the pseudo-element covers WebKit. */
  .fc-nav-summary { list-style: none; }
  .fc-nav-summary::-webkit-details-marker { display: none; }
  .fc-nav-summary::after {
    content: "";
    margin-left: 6px;
    border: 4px solid transparent;
    border-top-color: currentColor;
    transform: translateY(2px);
  }
  .fc-nav-group[open] > .fc-nav-summary::after {
    transform: translateY(-2px) rotate(180deg);
  }

  /* Open state gets the hover treatment, so it is obvious which dropdown
     the visible panel belongs to. */
  .fc-nav-group[open] > .fc-nav-summary {
    background: var(--fc-primary-light);
    color: var(--fc-link);
  }

  /* IN THE DRAWER A GROUP IS AN ACCORDION, NOT A DROPDOWN. An absolutely
     positioned panel inside a scrollable drawer is clipped by it, and even
     unclipped it would float over the entries it belongs to with nothing
     to say which. Indented in place, with a rule down the left, it reads
     as "these belong to More" at a glance and needs no positioning at all.
     Becomes a real dropdown again above the breakpoint. */
  .fc-nav-menu {
    position: static;
    margin: 2px 0 2px .9rem;
    padding: 0 0 0 .6rem;
    list-style: none;
    border-left: 2px solid var(--fc-border);
  }

  .fc-nav-menu .fc-nav-link {
    display: flex;
    width: 100%;
    white-space: normal;
  }

  /* ══ ABOVE THE BREAKPOINT: THE HORIZONTAL BAR ════════════════════════
     The drawer's summary goes, the list turns back into the row it has
     always been, and the sub-menus become real hover/click dropdowns.
     `nav.js` still opens them on a fine pointer — the hover path the owner
     asked for is untouched by any of this; only the CONTAINER changed. */
  @media (min-width: 54rem) {
    .fc-nav {
      position: static;
      z-index: auto;
    }

    .fc-nav-bar {
      flex-wrap: wrap;
      padding-block: .2rem;
    }

    /* `margin-left: auto` was the phone's way of pushing the toggle to the
       right edge. Up here the list itself is right-aligned inside a drawer
       that grows, so the auto margin is reset rather than left to fight
       flex-grow for the same free space. */
    .fc-nav-drawer { flex: 1 1 auto; margin-left: 0; }

    .fc-nav-panel {
      position: static;
      max-height: none;
      overflow: visible;
      padding: 0;
      background: none;
      border: 0;
      box-shadow: none;
    }

    .fc-nav-list {
      flex-direction: row;
      align-items: center;
      justify-content: flex-end;
    }

    .fc-nav-link,
    .fc-nav-summary { width: auto; }

    .fc-nav-current {
      background: none;
      box-shadow: inset 0 -2px 0 var(--fc-primary);
    }

    .fc-nav-menu {
      position: absolute;
      z-index: 40;
      top: 100%;
      left: 0;
      min-width: 220px;
      /* Never wider than the viewport: the last dropdowns in the row sit
         near the right edge, and a fixed min-width alone would push them
         off it. */
      max-width: min(20rem, calc(100vw - 2rem));
      margin: 0;
      padding: 6px;
      background: var(--fc-surface);
      border: 1px solid var(--fc-border);
      border-left-width: 1px;
      border-radius: var(--fc-radius-lg);
      box-shadow: 0 8px 24px rgb(15 23 42 / 12%);
    }

    /* The last two entries in the row are the two dropdowns, so anchoring
       their panels to the LEFT edge is what would push them off screen.
       Anchor to the right instead — it is the same rule the row's own
       geometry implies. */
    .fc-nav-item:nth-last-child(-n + 2) .fc-nav-menu {
      left: auto;
      right: 0;
    }
  }
}

/* ==========================================================================
   THE DESKTOP UN-COLLAPSE — one <details>, two layout modes

   Two controls in this application are a DISCLOSURE on a phone and simply
   OPEN on a desktop: the nav drawer (above) and the search page's filter
   bar (search/_filters.phtml). Both are native <details>, for the reasons
   layout/_nav.phtml's docblock argues at length — keyboard, announcement,
   and a control that still works with this CSP forbidding inline script.

   ─── WHY THIS IS NOT JUST `display: block` ─────────────────────────────
   A closed <details> hides its content in the UA stylesheet, and until
   recently there was no way to reach that from author CSS at all: the
   content lives behind an internal slot, so setting `display` on the
   child does nothing in Chromium. That is why the earlier nav shipped with
   "NO HAMBURGER, BY DECISION" — forcing a <details> open on desktop was
   correctly judged a hack with uneven support.

   That changed. The hiding is now specified on the ::details-content
   pseudo-element as `content-visibility`, and it is author-overridable —
   Chrome 131 (Nov 2024), Safari 18.4 (Mar 2025), Firefox 139 (May 2025).
   So the desktop layout no longer needs the element to be `open`; it needs
   one property.

   ─── THE FALLBACK IS THE POINT ─────────────────────────────────────────
   Everything here is inside @supports, INCLUDING the rules that hide the
   two summaries. An engine that cannot un-collapse the content therefore
   never hides the control that opens it: on such a browser the desktop
   simply keeps the "Menu" and "Filters" buttons. That is a smaller
   product, not a broken one — every entry and every filter is still one
   click away, still keyboard-operable, still announced.

   `nav.js` closes the remaining gap for the drawer by MEASURING the panel
   after load and opening the details if it came out with no height, so a
   browser that reports support but does not honour the override still
   ends up with a working bar. It is a second line of defence, not the
   mechanism: with scripting off, @supports alone decides.
   ========================================================================== */

@layer components {
  @media (min-width: 54rem) {
    @supports selector(details::details-content) {
      .fc-nav-drawer::details-content,
      .fc-filters::details-content {
        content-visibility: visible;
        block-size: auto;
      }

      .fc-nav-toggle,
      .fc-filters-summary { display: none; }
    }
  }
}

/* ==========================================================================
   VIEW MODES — Compact / Cards / Full (sub-project 2)

   In @layer components, like every block above, for the same reason: same
   cascade precedence as the rest of the design system.

   ONE `_row.phtml` serves all three. Everything here keys off `data-view`
   on the results list, and only TWO differences are structural (the cover
   image, and Full's pre-rendered body) — triplicating that partial's 583
   lines is the M4.8 share-link drift waiting to happen a second time.

   COMPACT gets no rules at all. It is the layout the page already had, so
   giving it a block would mean restating the existing list styling in a
   second place for two rules to drift apart.
   ========================================================================== */

@layer components {

  /* ── CARDS ──────────────────────────────────────────────────────────── */

  /* auto-fill, not auto-fit: with one result auto-fit would stretch a lone
     card across the whole width, which reads as a broken list rather than
     as one card. */
  .fc-item-list[data-view="cards"] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: 1rem;
    align-items: start;
  }

  .fc-item-list[data-view="cards"] > .fc-item {
    display: flex;
    flex-direction: column;
    height: 100%;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    overflow: hidden;
  }

  /* TWO SHAPES, ONE CLASS. With more than one picture the cover is a link
     to the item, aria-hidden with tabindex="-1": the title beside it links
     to the same place and the strip below carries every picture, so
     announcing it twice per card is noise. With exactly ONE picture there
     is no strip, the cover holds a real <button> that opens the lightbox,
     and it is in the tree because it is then the only way to the photo. */
  .fc-card-cover {
    display: block;
    aspect-ratio: 16 / 9;
    background: var(--fc-tag-bg);
  }

  .fc-card-cover img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* The button is a frame around the image and nothing else: a UA's own
     padding, border and background would draw a chrome box over the card's
     hero picture. `display: block` and the full height keep the whole
     frame clickable rather than just the image's own box. */
  .fc-card-cover-trigger {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
  }

  /* The focus ring is NOT removed with the border — this is the only route
     to the picture for a keyboard, and `:focus-visible` keeps the ring off
     a mouse click. Inset, because an outline on a flush-cropped hero would
     be clipped by the card's own overflow. */
  .fc-card-cover-trigger:focus-visible {
    outline: 2px solid var(--fc-link);
    outline-offset: -2px;
  }

  /* Full-bleed, as in Mosaic: negative margins run the cover out over the
     card's padding (.fc-item's .55rem .85rem) to its border, so it is the
     card's full width and the card's own clip rounds its top corners. */
  .fc-item-list[data-view="cards"] .fc-card-cover {
    margin: -.55rem -.85rem .6rem;
  }

  /* A card is read as a block, so its meta line wraps rather than
     competing with the title for one line. */
  .fc-item-list[data-view="cards"] .fc-item-meta {
    flex-wrap: wrap;
  }

  /* Actions stay dimmed-not-hidden here exactly as in the list (the brief
     forbids opacity:0 — a sighted keyboard user must be able to see what
     they have tabbed onto), but sit at the foot of the card. */
  .fc-item-list[data-view="cards"] .fc-item-actions {
    margin-top: auto;
  }

  /* ── FULL ───────────────────────────────────────────────────────────── */

  /* Full is Compact with every body already open, so the ONLY thing it
     needs is breathing room around a body that is now always there. The
     body itself is styled by .fc-item-body above and is not restated. */
  .fc-item-list[data-view="full"] .fc-item-body {
    margin-top: .6rem;
    padding-top: .6rem;
    border-top: 1px solid var(--fc-border);
  }
}

/* ==========================================================================
   THE POSTPONE TRAY (sub-project 3, task 1)

   In @layer components, like every block above and for the same reason:
   an UNLAYERED block would win over every layered one and could never be
   overridden by anything in this file.

   The tray is a native <details> sitting in the row's action row, so it
   inherits that row's dimmed-until-hover treatment for free and needs no
   opacity rules of its own. Its <summary> wears .fc-action-icon like every
   other control there and takes its whole appearance from that rule — the
   clock must not be a near-miss of the icons it sits between. All this
   block owns is (a) stripping the disclosure marker, which .fc-action-icon
   has no business knowing about, and (b) a popover that does not push the
   row's own layout around when it opens — hence `position: absolute`: a
   tray in the flow would reflow every row below it on every open.
   ========================================================================== */

@layer components {

  .fc-postpone { position: relative; display: inline-block; }

  /* Everything visual comes from .fc-action-icon, which this <summary> also
     wears — size, padding, colour, and the hover treatment. Restating any
     of it here would be a second definition to keep in sync, and since this
     block is LATER in the same layer it would silently win, which is how a
     control ends up looking almost like its neighbours.

     The two marker rules are the exception, because they are the one thing
     a <summary> needs and a <button> does not: without them the disclosure
     triangle sits beside the glyph and the control is 12px wider than every
     icon around it. */
  .fc-postpone-toggle::-webkit-details-marker { display: none; }
  .fc-postpone-toggle::marker { content: ''; }
  .fc-postpone[open] > .fc-postpone-toggle { border-color: var(--fc-link); color: var(--fc-link); }

  /* Left-anchored to the toggle itself: the tray is wider than the 28px
     control it hangs from, and on a narrow card a right-anchored popover
     would run off the left edge. */
  .fc-postpone-chips {
    position: absolute;
    z-index: 20;
    left: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem;
    margin-top: .25rem;
    padding: .35rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    box-shadow: var(--fc-shadow-lg);
  }

  /* Tighter than a plain .fc-action — four of these plus a date field have
     to fit inside a card — but never below --fc-tap in height, which is
     the one dimension a thumb actually needs. */
  .fc-postpone-chip { padding: .1rem .45rem; }

  /* The dialog opener. WAS an `<input type="date">` sitting in the tray —
     the rules for it lived here and are gone with it, because that field
     opened empty, carried no time and could not clear a due date. This is
     a plain .fc-action button; only the horizontal padding is set, so a
     two-word label ("Set a date…") does not read as cramped beside the
     four one-word chips. */
  .fc-postpone-pick { padding: .1rem .5rem; white-space: nowrap; }
}

/* ==========================================================================
   THE DUE-DATE DIALOG (items.js)

   In @layer components with everything else, and for the layer comment's
   reason: an unlayered block would outrank every rule above it and could
   never be overridden.

   It is a .fc-dialog like the QR and share dialogs — the backdrop, the
   width cap and the surface colour all come from there — with only the
   three things this dialog adds defined below.
   ========================================================================== */

@layer components {

  /* Wider than the 22rem default: a datetime-local renders a date field, a
     time field and a picker button side by side, and at 22rem Firefox wraps
     the time onto its own line. */
  .fc-due-dialog { max-width: min(92vw, 26rem); }

  .fc-due-dialog [data-due-field],
  .fc-due-dialog [data-due-text] { width: 100%; }

  /* The second label/field pair has no `.fc-field` wrapper of its own —
     one wasn't needed until there were two pairs to tell apart. */
  .fc-due-dialog [data-due-text] + .fc-label { margin-top: .75rem; }

  .fc-due-dialog [data-due-dialog-item] {
    margin: -.5rem 0 .75rem;
    overflow-wrap: anywhere;
  }

  /* The four shift chips, IN THE FLOW. `.fc-postpone-chips` is absolutely
     positioned in the row — it is a popover hanging off a 28px toggle —
     and inheriting that inside a dialog would park it over the buttons
     below. Everything else about the chips (gap, padding, border, surface)
     is inherited from that block on purpose, so the two sets of chips look
     like the same control in both places. */
  .fc-due-dialog .fc-due-shifts {
    position: static;
    margin: .5rem 0 0;
    box-shadow: none;
  }

  .fc-due-dialog [data-due-dialog-status]:empty { display: none; }

  .fc-due-keys { margin: .5rem 0 0; }

  /* The same badge as .fc-help-keys, .fc-view-key and .fc-leader-key —
     see the note on the last of those: a key you can press looks like one
     everywhere in this application, or it looks like four unrelated
     things. Restated rather than shared because those three each carry
     layout extras this one does not need; only the badge is common. */
  .fc-due-keys kbd {
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
  }

  /* Remove is pushed away from the Cancel/Save pair by a growing spacer, so
     a destructive button never sits under a thumb aiming for Save. The
     flex context is `.fc-form-actions`'s own — restating it here would be a
     second definition of the same row. */
  .fc-due-actions-spacer { flex: 1 1 auto; }
}

/* ==========================================================================
   THE ROW IMAGE STRIP AND ITS LIGHTBOX (sub-project 3, task 3)

   In @layer components, like every block above and for the same reason: an
   unlayered block would win over every layered one and could never be
   overridden by anything in this file.

   The strip is a horizontal scroller rather than a wrapping grid: a row is
   a row, and ten thumbnails wrapping to three lines would push the next
   result off the screen. `overflow-x: auto` keeps the row's height fixed
   whatever it holds — the same trick the nav list already uses.
   ========================================================================== */

@layer components {

  .fc-gallery {
    list-style: none;
    display: flex;
    gap: .4rem;
    margin: .5rem 0 0;
    padding: 0 0 .2rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }

  .fc-gallery-item { flex: 0 0 auto; }

  /* A button, because it opens a dialog — so it gets a button's reset
     rather than inheriting one. The visible focus ring is the whole reason
     the padding is here: without it the ring would sit on the image edge
     and be invisible against a dark photograph. */
  .fc-gallery-thumb {
    display: block;
    padding: 2px;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    cursor: pointer;
  }
  .fc-gallery-thumb:hover { border-color: var(--fc-link); }

  .fc-gallery-thumb img {
    display: block;
    width: 4.5rem;
    height: 4.5rem;
    object-fit: cover;
    border-radius: calc(var(--fc-radius) - 1px);
  }

  /* ── Attachments that are not pictures ────────────────────────────────
     Chips, not a strip: a PDF has no thumbnail, so the filename IS the
     control and it has to be readable at a glance. They WRAP (unlike the
     gallery, which scrolls) because a filename cannot be shrunk to a fixed
     4.5rem tile the way a photograph can, and a horizontally scrolled list
     of names hides the very thing that identifies them. `anywhere` keeps a
     40-character upload name from widening the row on a phone. */
  .fc-files {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin: .5rem 0 0;
    padding: 0;
  }

  .fc-file { flex: 0 1 auto; min-width: 0; }

  .fc-file-link {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    min-height: var(--fc-tap);
    padding: .15rem .5rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
    font-size: .85rem;
    overflow-wrap: anywhere;
  }
  .fc-file-link:hover { border-color: var(--fc-link); }

  .fc-file-icon { flex: 0 0 auto; opacity: .7; }

  /* FULL PAGE, not a card floating in the middle of one. A lightbox is for
     looking at a photograph, and the old `min(92vw, 70rem)` box spent most
     of a laptop screen on the backdrop. `100dvh` rather than `100vh` so a
     mobile browser's collapsing address bar cannot push the controls under
     the fold.

     ═══ `[open]` IS LOAD-BEARING, NOT TIDINESS ════════════════════════════
     This is the ONLY dialog rule in this file that sets `display`, and a
     `<dialog>` is hidden by `dialog:not([open]) { display: none }` in the
     UA stylesheet — which ANY author declaration outruns, specificity
     notwithstanding, because author styles beat UA styles outright. An
     unscoped `display: flex` here would leave the lightbox on screen, open
     or not, over every search result page in the app. The other dialogs
     never had to think about this because none of them sets `display`. */
  .fc-lightbox[open] {
    /* `inset: 0` and not `width: 100vw`: a modal dialog is laid out against
       the viewport, so this fills it exactly, while `100vw` INCLUDES the
       scrollbar gutter and would hang the dialog a scrollbar's width off
       the right edge on every desktop browser that shows one. The
       max-width/max-height resets are what release .fc-dialog's own
       `min(92vw, 22rem)` cap. */
    inset: 0;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    margin: 0;
    border: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: .5rem;
  }

  /* `contain`, never `cover`: a lightbox that crops the picture it exists
     to show is worse than the thumbnail it was opened from. `min-height: 0`
     is what lets the flex item actually shrink — without it the image
     keeps its intrinsic height and pushes the controls off screen. */
  .fc-lightbox img[data-lightbox-image] {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    max-width: 100%;
    object-fit: contain;
    margin: 0 auto;
    background: var(--fc-tag-bg);
  }

  .fc-lightbox [data-lightbox-caption] {
    margin: .5rem 0 0;
    text-align: center;
    word-break: break-word;
  }

  .fc-lightbox .fc-form-actions {
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    flex: 0 0 auto;
  }
  .fc-lightbox [data-lightbox-position] { margin: 0 .4rem; }

  .fc-lightbox [data-lightbox-caption],
  .fc-lightbox [data-lightbox-status] { flex: 0 0 auto; text-align: center; }

  /* An empty live region still occupies a line and pushes the controls
     down every time a delete fails and is then dismissed. */
  .fc-lightbox [data-lightbox-status]:empty { display: none; }

  /* `.fc-confirm` is a ROW everywhere else — two buttons beside the action
     they qualify, inside a row's flex action bar. Here it qualifies the
     whole dialog and carries a sentence above its buttons, so it stacks.
     Not a new class: the confirm is the same component and the same
     `hidden` toggling, laid out for a different container. */
  .fc-lightbox [data-lightbox-confirm] {
    flex: 0 0 auto;
    flex-direction: column;
    align-items: center;
    text-align: center;
    margin: 0;
  }

  .fc-lightbox .fc-confirm-text { margin: 0; }
}

/* ==========================================================================
   /account/password (sub-project 3, task 4)

   In @layer components, like every block above and for the same reason.
   One rule: the page reuses .fc-form, .fc-field, .fc-input, .error and
   .fc-flash exactly as they are — a bespoke look for the one page a user
   changes their password on would be the opposite of reassuring. What it
   adds is a list inside .error, which until now only ever held a single
   <p> and so has no rule for one.
   ========================================================================== */

@layer components {
  .fc-error-list { margin: 0; padding-left: 1.2rem; }
}

/* ==========================================================================
   KEYBOARD SHORTCUTS (spec §5) — the focus ring and the two dialogs

   IN THE STYLESHEET, WHICH IS THE WHOLE POINT. Legacy injected ~250 lines
   of CSS from JavaScript (`fluxyCo.js:_injectStyles()`); under a CSP with
   no 'unsafe-inline' that <style> element is silently dropped and the
   overlay renders naked. Here it is a file a server-side test can read.

   In @layer components, like every block above and for the same reason.
   ========================================================================== */

@layer components {

  /* The keyboard's own selection, which is NOT focus: shortcuts.js moves
     this class and never moves focus, so Tab keeps working and the caret
     stays in the search box. Two rings rather than one thick border — the
     inner one reads on a light surface, the outer on a dark one, and
     neither changes the row's box size, so `j` down a list cannot make the
     page reflow under the reader. */
  .fc-item.fc-kb-focused {
    box-shadow: 0 0 0 2px var(--fc-link), 0 0 0 5px rgb(37 99 235 / .18);
    border-radius: var(--fc-radius);
  }

  /* The selected row shows its actions in full, exactly as a hovered one
     does — a keyboard user is "pointing at" this row and must be able to
     read what they can do to it. */
  .fc-item.fc-kb-focused .fc-item-actions { opacity: 1; }

  @media (prefers-reduced-motion: no-preference) {
    .fc-item { transition: box-shadow .12s ease-in-out; }
  }

  /* ── The help overlay ─────────────────────────────────────────────── */

  /* Wider than a .fc-dialog, because it is a three-column table of keys
     and sentences rather than a question. */
  /* An explicit width, not only a max: a dialog shrinks to fit its content,
     and an auto-fit grid inside a shrink-to-fit box settles on ONE column. */
  .fc-help-dialog { width: min(94vw, 76rem); max-width: none; }

  /* Shortcuts and search syntax side by side once there is room for two
     ~30rem tables; below that they stack, shortcuts first. */
  .fc-help-columns {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr));
    gap: 1.25rem 2.5rem;
    align-items: start;
  }

  .fc-help-list {
    list-style: none;
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: baseline;
    gap: .3rem .8rem;
    margin: 0;
    padding: 0;
  }

  /* Each row is its own grid row spanning the three columns above. */
  .fc-help-row {
    display: contents;
  }

  /* Printed once per group — shortcuts.js leaves it empty on every row but
     the first of each group, so an empty cell is normal here, not a fault. */
  .fc-help-group {
    font-size: var(--fc-fs-sm);
    font-weight: 600;
    color: var(--fc-text-muted);
    white-space: nowrap;
  }

  .fc-help-keys {
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
    white-space: nowrap;
  }

  .fc-help-description { font-size: var(--fc-fs-sm); }

  /* The server-rendered search syntax beside the shortcut list — same grid. */
  .fc-help-intro {
    margin: 0 0 .75rem;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
  }
  .fc-help-list + .fc-help-intro { margin-top: .75rem; }

  /* ── The layout picker ────────────────────────────────────────────── */

  .fc-view-choices {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: .35rem;
    margin: 0 0 .5rem;
    padding: 0;
  }

  .fc-view-choice {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    padding: .35rem .6rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    color: var(--fc-text);
  }
  .fc-view-choice:hover { border-color: var(--fc-link); text-decoration: none; }

  .fc-view-choice-current {
    border-color: var(--fc-link);
    color: var(--fc-link);
    font-weight: 600;
  }

  /* The arrow keys move real focus rather than a private highlight, so
     this ring IS the selection — it has to be visible, not the 1px hint a
     mouse user never sees. `.fc-view-choice-current` also carries a link
     border, so the ring is offset rather than replacing it. */
  .fc-view-choice:focus-visible {
    outline: 2px solid var(--fc-link);
    outline-offset: 2px;
  }

  /* The letters you can type at this choice: the SHORTEST prefix that
     tells it apart from the others (ViewMode::keyHints()). */
  .fc-view-key {
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    font-weight: 400;
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
    color: var(--fc-text-muted);
  }

  /* An ambiguous prefix — `c` is both Compact and Cards. Marks the
     CANDIDATES; the focus ring above still marks the one that is selected,
     so the two never say the same thing. */
  .fc-view-choice-match { border-color: var(--fc-link); }

  /* ── The leader menu (`a`, `s`, `g`) ──────────────────────────────────
     Narrow on purpose. It is a list of one-letter choices, and the widest
     label on it is about thirty characters — a dialog sized like the help
     table would put a lone `d` at one end of forty rems of whitespace.

     Up to fifteen entries on a maximal row, so it scrolls rather than
     growing past the viewport on a short window; `max-height` is against
     the SMALL viewport unit, which is the one that accounts for a mobile
     browser's collapsing toolbar. */
  .fc-leader-menu { max-width: min(94vw, 22rem); }

  .fc-leader-menu-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: .2rem;
    max-height: min(60svh, 30rem);
    overflow-y: auto;
    margin: 0 0 .5rem;
    padding: 0;
  }

  /* A real <button>, so Tab, Enter and the arrows work from the platform
     and this menu is usable by someone who would rather point than type —
     which is what lets it teach the keys rather than only take them. */
  .fc-leader-choice {
    display: flex;
    align-items: baseline;
    gap: .6rem;
    width: 100%;
    font: inherit;
    font-size: var(--fc-fs-sm);
    text-align: left;
    padding: .3rem .5rem;
    border: 1px solid transparent;
    border-radius: var(--fc-radius);
    background: transparent;
    color: var(--fc-text);
    cursor: pointer;
  }
  .fc-leader-choice:hover { border-color: var(--fc-link); color: var(--fc-link); }
  .fc-leader-choice:focus-visible {
    outline: 2px solid var(--fc-link);
    outline-offset: 2px;
  }

  /* Deliberately the same badge as .fc-view-key and .fc-help-keys: a key
     you can press looks like one everywhere in this application, or it
     looks like three unrelated things. Fixed width so the labels align
     into a column instead of stepping. */
  .fc-leader-key {
    flex: none;
    min-width: 1.6rem;
    text-align: center;
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
    color: var(--fc-text-muted);
  }

  .fc-leader-label { flex: 1 1 auto; }

  .fc-view-status {
    margin: 0 0 .5rem;
    min-height: 1.2em;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
  }
}

/* ==========================================================================
   Rich-text item bodies (Task 9) — the WYSIWYG editor built by
   rich-editor.js and the format-choice radios in editor.phtml.

   In @layer components, like every block above and for the same reason:
   this file is the only place a strict CSP lets styling live, so a bare
   selector outside @layer would sit unlayered and could never be beaten by
   anything that follows it.

   Additive only, and every colour below is an existing semantic token —
   nothing new is declared in @layer tokens. `.fc-visually-hidden` and
   `.fc-check` are reused as-is (see @layer layout and the form-primitives
   block above); neither is redefined here.

   `.rich-editor-visual` and `.rich-editor-source` deliberately share one
   box recipe (border, radius, padding, min-height) so switching between
   them is a content swap, not a layout jump — the min-height matches
   `.fc-textarea`'s own 12-row default so the plain-textarea and rich-text
   editor states also agree with each other.
   ========================================================================== */

@layer components {

  .rich-editor {
    display: block;
    margin: 0 0 1rem;
  }

  .rich-editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    margin: 0 0 .4rem;
  }

  .rich-editor-toolbar button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--fc-tap);
    min-height: var(--fc-tap);
    padding: .25rem .5rem;
    font: inherit;
    font-size: var(--fc-fs-sm);
    line-height: 1.2;
    color: var(--fc-text);
    background: var(--fc-surface);
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
    cursor: pointer;
    transition: background var(--fc-transition), border-color var(--fc-transition);
  }
  .rich-editor-toolbar button:hover { border-color: var(--fc-link); }

  /* The HTML/Visual toggle reads as a separate action from the formatting
     buttons beside it, so it sits at the far end of the same row rather
     than blending into the run of them. */
  .rich-editor-toggle { margin-left: auto; }

  .rich-editor-visual,
  .rich-editor-source {
    display: block;
    width: 100%;
    min-height: 12rem;
    padding: .45rem .65rem;
    color: var(--fc-text);
    background: var(--fc-surface);
    border: 1px solid var(--fc-border-strong);
    border-radius: var(--fc-radius);
  }

  .rich-editor-visual {
    line-height: 1.55;
    font-size: var(--fc-fs-md);
    overflow-wrap: anywhere;
  }

  /* A contentEditable div gets none of a real textarea's default focus
     affordance (the base layer's `textarea:focus-visible` rule cannot
     match it), so the ring is drawn explicitly here — the same treatment
     `.fc-textarea:focus` uses, for one consistent focus language across
     the plain and rich editors. */
  .rich-editor-visual:focus-visible {
    outline: none;
    border-color: var(--fc-link);
    box-shadow: 0 0 0 3px var(--fc-primary-ring);
  }

  .rich-editor-source {
    resize: vertical;
    line-height: 1.55;
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-md);
  }

  /* Text colours ------------------------------------------------------------
     The classes RichTextSanitizer::TEXT_COLOURS lets into a stored body.
     Unscoped on purpose: a coloured body renders in the editor pane, the
     result rows, the item page and the modal alike. Each class sets
     --fc-ink so the toolbar swatch can reuse it — the swatch rule below
     outranks `.rich-editor-toolbar button { color }` on specificity, and a
     bare `.fc-color-red { color }` would lose to it. */
  .fc-color-red    { --fc-ink: var(--fc-ink-red);    color: var(--fc-ink); }
  .fc-color-blue   { --fc-ink: var(--fc-ink-blue);   color: var(--fc-ink); }
  .fc-color-green  { --fc-ink: var(--fc-ink-green);  color: var(--fc-ink); }
  .fc-color-amber  { --fc-ink: var(--fc-ink-amber);  color: var(--fc-ink); }
  .fc-color-purple { --fc-ink: var(--fc-ink-purple); color: var(--fc-ink); }

  .rich-editor-swatch-clear { --fc-ink: var(--fc-text-muted); }
  .rich-editor-toolbar button.rich-editor-swatch { color: var(--fc-ink); font-weight: 700; }

  /* Format-choice radios (editor.phtml) ----------------------------------- */

  .fc-label-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .5rem;
    margin: 0 0 .3rem;
  }

  /* Borderless: the row above already reads as a fieldset because it sits
     beside the Body label, so a second visible border here would just be
     noise. */
  .fc-format-choice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 1rem;
    margin: 0;
    padding: 0;
    border: 0;
  }
  .fc-format-choice .fc-check { margin: 0; }
}

/* ==========================================================================
   TOUCH HARDENING — last block in the file, and that is deliberate

   Every rule here is a LATE override of something defined earlier in this
   same layer. @layer settles precedence between layers, not within one, so
   inside `components` the last matching rule of equal specificity wins —
   which means this block only works where it sits. Do not move it up the
   file to group it with the control it patches, and do not raise its
   specificity to make a move safe; the position IS the mechanism, and it
   is the same reason the file already appends rather than interleaves.
   ========================================================================== */

@layer components {

  /* ═══ THE 16px FLOOR ON TOUCH — A BUG FIX, NOT A TYPE DECISION ═════════
     Mobile Safari zooms the entire document when a form control it is
     focusing computes below 16px, and it never zooms back out: the user is
     left on a page wider than the screen, panning sideways to find the
     rest of the form. Every control carrying a size from the small end of
     the type scale is affected — the per-page <select>, the editor's
     <textarea>, the compact admin inputs, the file picker. The omnibox and
     .fc-input escape it only because they say `font: inherit` and the body
     is 16px; that is why the omnibox rule says so in a comment.

     Scoped to `pointer: coarse` — the same gate --fc-tap already uses — so
     the dense laptop type scale this tool is built around is untouched.
     16px is a literal rather than a token on purpose: it is Safari's
     threshold, not a step of our scale, and expressing it as --fc-fs-*
     would silently re-break the day that scale is retuned. */
  @media (pointer: coarse) {
    .fc-select,
    .fc-textarea,
    .fc-input-small,
    .upload-area input[type="file"] { font-size: 16px; }
  }

  /* The queue's per-file Remove. It is a bare button with its own class,
     so it inherits none of the --fc-tap floors the .fc-btn family carries,
     and it sits in a list of rows a thumb has to hit one of exactly. Same
     coarse-pointer gate, same token, stated here for the same reason the
     block above is: this is a late override and the position is why it
     works. */
  @media (pointer: coarse) {
    .upload-pending-remove { min-height: var(--fc-tap); min-width: var(--fc-tap); }
  }
}

/* ============================================================================
   The duplicates tool (/tools/duplicates). Appended as its own block for
   the reason every late block in this file gives: a contiguous region, the
   same cascade precedence as every other component. Tokens only.
   ========================================================================== */

@layer components {
  .fc-dup-group {
    margin: 0 0 1.25rem;
    padding: .9rem 1rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
  }
  .fc-dup-url {
    margin: 0 0 .6rem;
    font-size: var(--fc-fs-md);
    font-weight: 600;
    overflow-wrap: anywhere;
    display: flex; flex-wrap: wrap; gap: .5rem; align-items: baseline;
  }
  .fc-dup-items { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
  .fc-dup-item {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: .75rem;
    padding: .5rem .6rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
  }
  .fc-dup-choice { display: grid; gap: .25rem; align-content: start; font-size: var(--fc-fs-sm); white-space: nowrap; }
  .fc-dup-choice label { display: flex; align-items: center; gap: .3rem; min-height: var(--fc-tap); }
  .fc-dup-title { font-weight: 600; }
  .fc-dup-meta {
    margin: .25rem 0 0;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text-muted);
    display: flex; flex-wrap: wrap; gap: .25rem .75rem;
  }
  .fc-dup-actions { margin: .75rem 0 0; }
  .fc-dup-plan { margin: 0 0 1rem; padding-left: 1.2rem; display: grid; gap: .35rem; }
  .fc-dup-plan-remove { color: var(--fc-danger-text); }
}

/* ============================================================================
   THE INLINE TAG EDITOR — a row's tag list, opened in place (tag-edit.js)

   Appended as its own block for the reason every late block gives: one
   contiguous region, the same cascade precedence as every other component.
   Tokens only, so dark mode needs nothing here.

   THE VISUAL GRAMMAR, in three lines:
   - The OPENER is a ghost chip at the end of the tag list: dashed, muted,
     "+ tag" when it is the only thing there and a bare "+" beside real
     chips. The word is shown by `:first-child`, never by script.
   - EDITING is the same "accent-bordered box" the inline title editor
     already means by editing (.fc-inline-title-input): the list grows a
     1px --fc-accent frame, the chips stay chips, the last slot is a
     borderless field that reads as the next, empty chip. One focus ring on
     the BOX while the field has focus — the field has no border of its
     own to hang a ring on, and a floating rectangle inside the box would
     be a second frame.
   - The SUGGESTION popup echoes .fc-omnibox-list (same surface, radius,
     shadow, z-index, the same dvh cap and overscroll rule, for the same
     phone-keyboard reasons documented there) so the two comboboxes on this
     page read as one control. The highlight is `[aria-selected="true"]`,
     the attribute the script announces — not a class it could forget.

   `[hidden]` (base layer, !important) hides the opener's <li> and the
   popup regardless of anything set here; nothing below re-declares
   `display` on either of them for that reason.
   ========================================================================== */

@layer components {

  /* ── the opener ────────────────────────────────────────────────────── */

  .fc-tag-add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .15rem;
    min-height: 1.5rem;
    min-width: 1.5rem;
    padding: 0 .35rem;
    font: inherit;
    font-size: var(--fc-fs-sm);
    line-height: 1;
    color: var(--fc-text-muted);
    background: transparent;
    border: 1px dashed var(--fc-border-strong);
    border-radius: 9999px;
    cursor: pointer;
  }
  .fc-tag-add:hover,
  .fc-tag-add:focus-visible {
    color: var(--fc-link);
    border-color: var(--fc-link);
    background: var(--fc-primary-light);
  }
  .fc-tag-add-glyph { font-size: var(--fc-fs-md); }

  /* "tag" is spelled out only when the opener is alone in the list — the
     empty state needs a word, a row of chips needs a plus. */
  .fc-tag-add-label { display: none; }
  .fc-tag-add-item:first-child .fc-tag-add-label { display: inline; }
  .fc-tag-add-item:first-child .fc-tag-add { padding: 0 .6rem 0 .45rem; }

  /* ── the editing box ───────────────────────────────────────────────── */

  .fc-tag-list[data-editing] {
    position: relative;
    align-items: center;
    gap: .3rem;
    padding: .3rem;
    border: 1px solid var(--fc-accent);
    border-radius: var(--fc-radius);
    background: var(--fc-surface);
  }
  /* The ring belongs to the box only while the FIELD has focus; a focused
     remove button draws its own base-layer ring, and two rings at once
     would say "two things are focused". */
  .fc-tag-list[data-editing]:has(.fc-tag-input:focus-visible) {
    outline: 2px solid var(--fc-link);
    outline-offset: 2px;
  }

  .fc-tag-edit-chip {
    display: inline-flex;
    align-items: center;
    gap: .1rem;
    min-height: 1.5rem;
    padding: 0 0 0 9px;
    border-radius: 9999px;
    font-size: var(--fc-fs-sm);
    background: var(--fc-tag-bg);
    color: var(--fc-tag-text);
  }

  /* 24px square: the WCAG 2.2 target minimum, and exactly the chip's
     height so the pill needs no extra room for it. Danger colours on
     hover/focus, and only then — a row of red crosses at rest would make
     every chip look like a warning. */
  .fc-tag-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    margin: 0;
    border: 0;
    border-radius: 9999px;
    background: transparent;
    color: inherit;
    cursor: pointer;
  }
  .fc-tag-remove:hover,
  .fc-tag-remove:focus-visible {
    background: var(--fc-danger-bg);
    color: var(--fc-danger-text);
  }
  .fc-tag-remove:focus-visible { outline-offset: 0; }
  .fc-tag-remove-glyph {
    width: .85rem;
    height: .85rem;
    flex: none;
    pointer-events: none;
  }

  /* ── the input slot ────────────────────────────────────────────────── */

  /* The slot's <li> gives up its box so the field, the popup and the hint
     are laid out by the list itself: the field fills the rest of the
     current line, the hint takes a whole one. */
  .fc-tag-edit-slot { display: contents; }

  .fc-tag-input {
    flex: 1 1 6rem;
    min-width: 6rem;
    min-height: 1.5rem;
    padding: 0 .4rem;
    font: inherit;
    font-size: var(--fc-fs-sm);
    color: var(--fc-text);
    background: transparent;
    border: 0;
    border-radius: 9999px;
  }
  .fc-tag-input::placeholder { color: var(--fc-text-muted); }
  /* The box carries the ring (above); the base-layer outline on the field
     would draw a second one inside it. */
  .fc-tag-input:focus-visible { outline: none; }

  .fc-tag-suggest {
    position: absolute;
    top: calc(100% + .25rem);
    left: 0;
    z-index: 20;
    min-width: min(12rem, 100%);
    max-width: 100%;
    margin: 0;
    padding: .25rem;
    list-style: none;
    max-height: min(14rem, 40dvh);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--fc-surface);
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    box-shadow: var(--fc-shadow-lg);
  }
  .fc-tag-suggest:empty { display: none; }

  .fc-tag-suggest [role="option"] {
    display: flex;
    align-items: center;
    min-height: var(--fc-tap);
    padding: .3rem .6rem;
    border-radius: var(--fc-radius);
    font-size: var(--fc-fs-sm);
    cursor: pointer;
  }
  /* :hover is separate on purpose, as in the omnibox — mousing over the
     list must not silently change what Enter would accept. */
  .fc-tag-suggest [role="option"][aria-selected="true"] {
    background: var(--fc-primary-light);
    color: var(--fc-link);
  }
  .fc-tag-suggest [role="option"]:hover { background: var(--fc-tag-bg); }

  .fc-tag-edit-keys {
    flex: 1 1 100%;
    margin: .1rem 0 0;
    padding: 0 .2rem;
  }
  /* Same badge as .fc-due-keys kbd — see the note there on why it is
     restated rather than shared. */
  .fc-tag-edit-keys kbd {
    font-family: var(--fc-font-mono);
    font-size: var(--fc-fs-sm);
    padding: .05rem .4rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius);
    background: var(--fc-tag-bg);
  }

  /* A card clips its own overflow (the cover image's corners); a popup
     hanging off a card's tag list would be cut at the card's edge. Lifted
     only while that card is editing, so the corners stay clipped the rest
     of the time. */
  .fc-item-list[data-view="cards"] > .fc-item:has(.fc-tag-list[data-editing]) { overflow: visible; }

  /* ── touch ─────────────────────────────────────────────────────────── */

  @media (pointer: coarse) {
    .fc-tag-add,
    .fc-tag-edit-chip,
    .fc-tag-remove { min-height: var(--fc-tap); }
    .fc-tag-add,
    .fc-tag-remove { min-width: var(--fc-tap); }
    .fc-tag-remove { height: auto; width: auto; }
    /* Safari's 16px zoom threshold — the same bug fix, and the same
       literal for the same reason, as the TOUCH HARDENING block. */
    .fc-tag-input { font-size: 16px; }
    /* No Tab, no Esc, no Backspace to name on an on-screen keyboard. */
    .fc-tag-edit-keys { display: none; }
  }
}

/* ==========================================================================
   THE TITLE FIELD'S CHARACTER COUNTER. items.title is VARCHAR(190) and the
   server refuses a longer title with a banner (it used to be a 500);
   editor.js keeps "n / 190" current while typing and turns it red past the
   wall. Muted by default so it never competes with the smart-field hint it
   shares a line with; the hint row wraps on narrow screens rather than
   pushing the number off the edge. In @layer components once more, for the
   same reason every block above is: unlayered rules would win over every
   layered one and could never be overridden.
   ========================================================================== */

@layer components {
  .fc-hint-row { display: flex; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
  .fc-count { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--fc-text-muted); }
  .fc-count.is-over { color: var(--fc-danger-text); font-weight: 600; }
}

/* ==========================================================================
   VIEW MODE — MOSAIC (added 2026-09-14; replaces Table, 2026-09-13)

   The fourth layout beside Compact / Cards / Full: a MASONRY of tiles,
   each with its cover (natural ratio, cropped only when very tall), the
   title, a four-line plain-text excerpt of the body, the URL's host and
   one quiet meta line. For browsing rather than scanning — the person
   looking through what they saved, not the person checking what is due.
   Table was withdrawn after a day (too dense, columns broke, not far
   enough from Compact); this is the opposite bet: fewer rows per screen,
   and each one tells you something the title does not.

   In @layer components, like every block above and for the same reason:
   an unlayered rule would win over every layered one and could never be
   overridden — a mistake this file has already paid for once.

   CSS MULTI-COLUMN, NO SCRIPT, NO BREAKPOINTS. `columns: 16rem` gives the
   browser a column WIDTH and lets it derive the count: a phone gets one,
   a laptop three, a wide screen five, with no media query to keep in step
   with the shell's. Items flow down the first column and then the next,
   which is also DOM order — so `j`/`k` walk the mosaic in the order the
   eye reads it. Each tile is `inline-block; width: 100%` rather than
   `block`: a block's bottom margin is split across a column break and
   leaves a tile flush against the next column's top; an inline-block's
   is not (the classic multicol masonry fix).

   THE ROW GROWS TWO LINES HERE AND NOWHERE ELSE. `.fc-item-excerpt` and
   `.fc-item-host` are emitted by _row.phtml only when
   SearchController::withExcerpts() attached `excerpt`, which it does for
   this mode alone; Compact, Cards and Full render byte-for-byte what they
   did. The cover is the Cards cover (withCovers attaches it to both tile
   layouts), restyled here to its natural ratio — mixed heights are what
   make a mosaic a mosaic, so there is no aspect-ratio lock.

   ACTIONS APPEAR ON HOVER — Matteo's explicit ask for this mode, and the
   one place this file departs from "dimmed, never hidden" (the base
   actions rule is untouched; only this mode's override differs).
   With a mouse the bar is COLLAPSED at rest — no height, no margin, no
   opacity — and the tile GROWS to fit it on hover, in flow, so it never
   lies over the tags and the due chip (a first draft floated it over the
   foot, and Matteo could not click a tag). The column below shifts by the
   bar's height while the pointer is on the tile; he chose that over a
   permanent blank band on every tile. `max-height`, never `display:
   none` or `visibility: hidden`, so every control stays in the tab order,
   and the collapsed state is written as `:not(:has(:focus-visible))`: the
   bar opens the moment Tab lands on a control, and stays shut after a
   mouse click on one — a click focuses the button too, and `:focus-within`
   kept the bar open after Expand/Collapse until focus moved. The
   `.fc-kb-focused` tile does NOT open its bar in this mode: Matteo wants
   the actions gone unless hovered, and the row shortcuts work without
   seeing them. Only under `(hover: hover)`: a phone has no hover and
   keeps the base dimmed-but-visible bar in flow.

   WHAT IT KEEPS
   - the urgency ramp on the <li>: tint plus 3px left bar, untouched. The
     tile selectors are wrapped in :where() so they weigh (0,1,0) and the
     `.fc-item.duedate*` rules (0,2,0) beat them; the hover lift is a
     box-shadow, not a border, so it cannot repaint the bar's colour;
   - the keyboard ring: a box-shadow on the tile itself, which the tile's
     own `overflow: hidden` does not clip (overflow clips descendants,
     never the element's own shadow) and which the 1rem column gap has
     room for. The hover lift is excluded from a selected tile so the two
     shadows never fight;
   - --fc-tap on every control, and the two popups that hang off a row
     (the tag editor, the postpone tray) lift the overflow clip while
     open, exactly as Cards does for the first.

   CONTRAST, MEASURED. The excerpt is body text at --fc-fs-sm and needs
   4.5:1; --fc-text-muted sits at 4.35:1 on the past tint and 4.48:1 on
   today's, so it takes --fc-tag-text instead: 6.9–7.6:1 in light, 9.9+ in
   dark, on every tile background. The same swap the badge rule made, for
   the same reason. The host line is the meta line's size and colour.
   ========================================================================== */

@layer components {

  /* ── the columns ────────────────────────────────────────────────────── */

  .fc-item-list[data-view="mosaic"] {
    display: block;
    columns: 16rem;
    column-gap: 1rem;
  }

  /* ── the tile ───────────────────────────────────────────────────────── */

  :where(.fc-item-list[data-view="mosaic"]) > .fc-item {
    position: relative;
    display: inline-block;
    width: 100%;
    vertical-align: top;
    margin: 0 0 1rem;
    padding: .9rem;
    break-inside: avoid;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    overflow: hidden;
  }

  /* A soft lift, and only on a tile the keyboard is not pointing at: the
     ring is a box-shadow too, and the selected tile keeps its ring. */
  :where(.fc-item-list[data-view="mosaic"]) > .fc-item:hover:not(.fc-kb-focused) {
    box-shadow: var(--fc-shadow-lg);
  }

  @media (prefers-reduced-motion: no-preference) {
    :where(.fc-item-list[data-view="mosaic"]) > .fc-item {
      transition: border-color var(--fc-transition), box-shadow var(--fc-transition);
    }
  }

  /* The two popups that hang below a row would be cut at the tile's edge
     while it clips its cover's corners; lifted only while one is open. */
  :where(.fc-item-list[data-view="mosaic"]) > .fc-item:has(.fc-tag-list[data-editing]),
  :where(.fc-item-list[data-view="mosaic"]) > .fc-item:has(.fc-postpone[open]) {
    overflow: visible;
  }

  /* ── the cover: full-bleed, natural ratio, capped ───────────────────── */

  /* Negative margins run it out over the tile's padding to the border;
     `aspect-ratio: auto` undoes the Cards 16:9 lock. The cap is on the
     frame AND the image so a tall portrait crops (object-fit: cover from
     the base rule) instead of running the whole column. */
  .fc-item-list[data-view="mosaic"] .fc-card-cover {
    aspect-ratio: auto;
    max-height: 18rem;
    margin: -.9rem -.9rem .75rem;
  }

  .fc-item-list[data-view="mosaic"] .fc-card-cover img {
    height: auto;
    max-height: 18rem;
  }

  /* ── type: title one size down, then the two new lines ──────────────── */

  .fc-item-list[data-view="mosaic"] .fc-item-title {
    font-size: var(--fc-fs-md);
    font-weight: 600;
    line-height: 1.35;
    overflow-wrap: anywhere;
  }

  .fc-item-list[data-view="mosaic"] .fc-item-excerpt {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    overflow: hidden;
    margin: .45rem 0 0;
    font-size: var(--fc-fs-sm);
    line-height: 1.45;
    color: var(--fc-tag-text);
    overflow-wrap: anywhere;
  }

  .fc-item-list[data-view="mosaic"] .fc-item-host {
    display: block;
    margin-top: .4rem;
    font-size: var(--fc-fs-xs);
    color: var(--fc-text-muted);
    overflow-wrap: anywhere;
  }

  /* ── the foot: one meta line, then chips ────────────────────────────── */

  .fc-item-list[data-view="mosaic"] .fc-item-meta { margin-top: .55rem; }
  .fc-item-list[data-view="mosaic"] .fc-tag-list,
  .fc-item-list[data-view="mosaic"] .fc-group-chips { margin-top: .45rem; }

  /* ── actions: in flow at the foot; collapsed until hovered, with a mouse ── */

  .fc-item-list[data-view="mosaic"] .fc-item-actions {
    justify-content: flex-end;
    margin-top: .6rem;
  }

  @media (hover: hover) {
    /* Collapsed unless keyboard focus is inside it — stated in the
       selector, so the rule cannot reach a bar a keyboard user has tabbed
       onto. That is also what satisfies testActionsAreDimmedNotHiddenAnd
       ComeBackOnFocusWithin: the guard greps for a bare actions selector
       that zeroes the opacity, and a rule that excludes focus is not that
       thing. Do not spell the bare selector out in a comment here: the
       guard's regex reads comments too. */
    .fc-item-list[data-view="mosaic"] .fc-item-actions:not(:has(:focus-visible)) {
      max-height: 0;
      margin-top: 0;
      opacity: 0;
      overflow: hidden;
    }

    .fc-item-list[data-view="mosaic"] .fc-item:hover .fc-item-actions,
    .fc-item-list[data-view="mosaic"] .fc-item-actions:has(:focus-visible) {
      max-height: 16rem;
      margin-top: .6rem;
      opacity: 1;
      overflow: visible;
    }

    @media (prefers-reduced-motion: no-preference) {
      .fc-item-list[data-view="mosaic"] .fc-item-actions {
        transition: max-height var(--fc-transition), margin-top var(--fc-transition), opacity var(--fc-transition);
      }
    }
  }
}

/* ==========================================================================
   VIEW MODE — SLIM (added 2026-09-24)

   The fifth layout: ONE LINE per item, carrying only what identifies it —
   the title, one date and the tags. Everything else the row renders
   (type glyph, badges, cover, group chips, owner, Open) is hidden here
   and nowhere else — the gallery and files only until the row is
   expanded; the markup is the shared _row.phtml,
   untouched, and the controller attaches nothing for this mode.

   THE LINE IS A GRID on the <li>: title | tags | date. Whatever the row
   opens below itself — the expanded body, a status message, the tag
   editor, a delete or reorganize confirm, the postpone tray — drops to a
   full-width line underneath, so opening something makes the row taller
   and never pushes the line sideways.

   ONE DATE. The meta line emits Due, then Updated (only on a different
   day from Created — the same-day rule), then Created. Slim keeps the
   first of those: the due date when there is one (it keeps its "Due"
   word, since a deadline and a save date must not look alike), else the
   most recent save date, shown bare. The label word of a save date is
   dropped with `font-size: 0` on the span and restored on its <time>.

   DONE is read from the Done badge, which items.js keeps current on
   toggle (data-completed on the <li> is server-only and would go stale):
   the title is struck through and muted instead of wearing a badge.

   ACTIONS ON HOVER ONLY, like Mosaic, and for the same reasons written
   the same way: `:not(:has(:focus-visible))` so Tab still lands on a
   visible control, opacity rather than display so every control stays in
   the tab order. The bar sits OVER the tail of the title (same grid
   cell, justified to its end, the row's own background inherited so the
   urgency tint carries through) — never over the tags or the date, which
   stay clickable. The j/k selection does NOT reveal it: the row
   shortcuts act on the selected row whether or not the icons show. An
   open confirm or postpone tray pins it visible on its own line. Without
   hover (a phone) the bar sits in flow on a line of its own.
   ========================================================================== */

@layer components {

  .fc-item-list[data-view="slim"] { gap: .15rem; }

  /* :where() keeps the row at (0,1,0) so .fc-item.duedate* (0,2,0) still
     paints the urgency tint and left bar. */
  :where(.fc-item-list[data-view="slim"]) > .fc-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    column-gap: .75rem;
    padding: .2rem .6rem;
    border-color: transparent;
    background: transparent;
  }
  /* Three sides only: the left one is the urgency bar's colour. */
  :where(.fc-item-list[data-view="slim"]) > .fc-item:hover {
    border-top-color: var(--fc-border);
    border-right-color: var(--fc-border);
    border-bottom-color: var(--fc-border);
    background: var(--fc-surface);
  }

  /* Everything that is not the line itself runs full width below it. */
  .fc-item-list[data-view="slim"] > .fc-item > * { grid-column: 1 / -1; }

  /* ── what Slim leaves out ───────────────────────────────────────────── */

  .fc-item-list[data-view="slim"] :is(
    .fc-card-cover, .fc-item-type, .fc-badges, .fc-item-excerpt, .fc-item-host,
    .fc-group-chips, .fc-meta-owner, .fc-meta-edit
  ) { display: none; }

  /* Pictures and files are left out of the LINE, not out of the item: once
     the row is expanded (x / Expand) they come back, full width under the
     body, because the reader asked to see the whole item. The body's own
     `hidden` is the switch — items.js flips it on every toggle. */
  .fc-item-list[data-view="slim"] > .fc-item:not(:has(> .fc-item-body:not([hidden]))) > :is(.fc-gallery, .fc-files) {
    display: none;
  }

  /* ── title ──────────────────────────────────────────────────────────── */

  .fc-item-list[data-view="slim"] > .fc-item > .fc-item-header {
    grid-column: 1;
    grid-row: 1;
    flex-wrap: nowrap;
    min-width: 0;
  }

  .fc-item-list[data-view="slim"] .fc-item-title {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    font-size: var(--fc-fs-md);
    font-weight: 500;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .fc-item-list[data-view="slim"] .fc-item:has(.fc-badge-done) .fc-item-link {
    color: var(--fc-text-muted);
    text-decoration: line-through;
  }

  /* ── tags: one line, cut off rather than wrapped ────────────────────── */

  .fc-item-list[data-view="slim"] > .fc-item > .fc-tag-list:not([data-editing]) {
    grid-column: 2;
    grid-row: 1;
    flex-wrap: nowrap;
    max-width: min(40vw, 24rem);
    margin: 0;
    overflow: hidden;
  }
  .fc-item-list[data-view="slim"] .fc-tag-list:not([data-editing]) > li { flex: none; }
  .fc-item-list[data-view="slim"] .fc-tag-chip { min-height: 1.3rem; font-size: var(--fc-fs-xs); }

  /* ── date: the first one the meta line carries ──────────────────────── */

  .fc-item-list[data-view="slim"] > .fc-item > .fc-item-meta {
    grid-column: 3;
    grid-row: 1;
    flex-wrap: nowrap;
    margin: 0;
    white-space: nowrap;
  }
  .fc-item-list[data-view="slim"] :is(.fc-meta-due, .fc-meta-date) ~ .fc-meta-date { display: none; }
  /* Sorted by created date: the created date is the one shown (u:/c: label
     tells which), unless a due date already holds the slot. */
  .fc-item-list[data-view="slim"][data-sort="ByCreateDate"] .fc-meta-updated { display: none; }
  .fc-item-list[data-view="slim"][data-sort="ByCreateDate"] .fc-item-meta:not(:has(.fc-meta-due)) .fc-meta-created { display: inline; }

  /* ── actions ────────────────────────────────────────────────────────── */

  .fc-item-list[data-view="slim"] > .fc-item > .fc-item-actions {
    flex-wrap: nowrap;
    gap: .1rem;
    margin: 0;
  }

  /* An open confirm or tray takes a line of its own and stays shown. */
  .fc-item-list[data-view="slim"] > .fc-item:has(.fc-confirm:not([hidden]), .fc-postpone[open]) > .fc-item-actions {
    flex-wrap: wrap;
    margin-top: .3rem;
    opacity: 1;
  }

  @media (hover: hover) {
    .fc-item-list[data-view="slim"] > .fc-item:not(:has(.fc-confirm:not([hidden]), .fc-postpone[open])) > .fc-item-actions {
      grid-column: 1;
      grid-row: 1;
      justify-self: end;
      z-index: 1;
      padding-left: .5rem;
      background: inherit;
      border-radius: var(--fc-radius);
    }

    /* Selector names focus explicitly — see the Mosaic block for why the
       CSS guard test requires that shape. */
    .fc-item-list[data-view="slim"] > .fc-item:not(:hover, :has(.fc-confirm:not([hidden]), .fc-postpone[open])) > .fc-item-actions:not(:has(:focus-visible)) {
      opacity: 0;
      pointer-events: none;
    }

    .fc-item-list[data-view="slim"] > .fc-item:not(:hover) .fc-tag-add-item:not(:has(:focus-visible)) {
      opacity: 0;
    }
  }

  /* The row's own hover background is --fc-surface; a tinted (due) row
     keeps its tint, and `background: inherit` above follows either. */
  :where(.fc-item-list[data-view="slim"]) > .fc-item.duedatePast:hover  { background: var(--fc-due-past-bg); }
  :where(.fc-item-list[data-view="slim"]) > .fc-item.duedateToday:hover { background: var(--fc-due-today-bg); }
  :where(.fc-item-list[data-view="slim"]) > .fc-item.duedateNear:hover  { background: var(--fc-due-near-bg); }
  :where(.fc-item-list[data-view="slim"]) > .fc-item.duedateWeek:hover  { background: var(--fc-due-week-bg); }
  :where(.fc-item-list[data-view="slim"]) > .fc-item.duedateFar:hover   { background: var(--fc-due-far-bg); }
}

/* ── Batch selection (spec 2026-09-27) ─────────────────────────────────
   Keyed on [data-batch], not on a layout, so a layout that opts in via
   ViewMode::supportsBatch() gets it without a CSS change beyond spacing.
   The checkbox sits in the row's left padding, OUTSIDE the grid, so the
   layout's own columns are untouched. Hidden until hover, keyboard focus,
   or any selection on the page; always shown without hover (touch). */
@layer components {
  .fc-item-list[data-batch] > .fc-item { position: relative; padding-left: 2rem; }

  .fc-item-list[data-batch] > .fc-item > .fc-batch-check {
    /* Auto lines: Slim spans every child across 1 / -1, which would place
       the box inside the content box — on top of the title, not in the
       padding. */
    grid-column: auto;
    grid-row: auto;
    position: absolute;
    top: .45rem;
    left: .6rem;
    width: 1rem;
    height: 1rem;
    margin: 0;
    accent-color: var(--fc-link);
  }

  @media (hover: hover) {
    .fc-item-list[data-batch]:not([data-batch-active]) > .fc-item:not(:hover) > .fc-batch-check:not(:focus-visible) {
      opacity: 0;
    }
  }

  .fc-batch-bar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem .6rem;
    margin: 0 0 .5rem;
    padding: .4rem .6rem;
    border: 1px solid var(--fc-border);
    background: var(--fc-surface);
  }
  .fc-batch-bar[hidden] { display: none; }

  .fc-batch-all { display: inline-flex; align-items: center; gap: .4rem; font-weight: 600; }

  .fc-batch-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .3rem;
    margin: 0;
    padding: 0;
    border: 0;
  }

  .fc-batch-menu { position: relative; }
  .fc-batch-menu > summary { list-style: none; cursor: pointer; }
  .fc-batch-menu > summary::-webkit-details-marker { display: none; }

  .fc-batch-pop {
    position: absolute;
    top: calc(100% + .25rem);
    left: 0;
    z-index: 6;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem;
    min-width: 16rem;
    padding: .5rem;
    border: 1px solid var(--fc-border);
    background: var(--fc-surface);
  }

  .fc-batch-field { display: inline-flex; align-items: center; gap: .35rem; }

  .fc-batch-status { flex-basis: 100%; margin: 0; }
  .fc-batch-status:empty { display: none; }
}

/* ── QR tool: type tabs, inline code, field errors ─────────────────────
   In @layer components, like every block above and for the same reason.
   The tabs reuse .fc-bar-pill; the current one takes the "checked" look
   of the search pills. The inline <svg> is QrSvgRenderer's output, whose
   colours are presentation attributes — only its BOX is sized here, never
   its fill (a dark-theme rule recolouring it would make it unscannable). */
@layer components {
  .fc-qr-types ul { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; padding: 0; }
  .fc-qr-types .fc-bar-pill:hover { text-decoration: none; }
  .fc-qr-type-current { border-color: var(--fc-link); background: var(--fc-primary-light); color: var(--fc-link); font-weight: 600; }
  .fc-qr-types .fc-bar-pill:focus-visible { outline: 2px solid var(--fc-link); outline-offset: 2px; }

  .fc-textarea-short { min-height: 5rem; }

  .fc-field-error { margin: .3rem 0 0; font-size: var(--fc-fs-sm); color: var(--fc-danger-text); }

  .fc-qr-figure svg { display: block; width: min(100%, 288px); height: auto; }

  /* The optional style block: a <details> dressed as .fc-fieldset, shut
     by default so the plain path stays one field and one button. */
  .fc-qr-style { margin: 1rem 0; }
  .fc-qr-style > summary { cursor: pointer; font-weight: 600; min-height: var(--fc-tap); display: flex; align-items: center; gap: .35rem; }
  .fc-qr-style[open] > summary { margin-bottom: .5rem; }
  .fc-qr-optional { font-weight: 400; color: var(--fc-text-muted); }
  .fc-qr-colours { display: flex; flex-wrap: wrap; gap: 0 1.5rem; }
  .fc-qr-colour { display: block; width: 4.5rem; height: var(--fc-tap); padding: 2px; border: 1px solid var(--fc-border-strong); border-radius: var(--fc-radius); background: var(--fc-surface); cursor: pointer; }
  .fc-qr-colour:focus-visible { outline: 2px solid var(--fc-link); outline-offset: 2px; }
  .fc-qr-logo-current { display: flex; align-items: center; gap: .75rem; margin-bottom: .5rem; }
  .fc-qr-logo-current img { object-fit: contain; border: 1px solid var(--fc-border); border-radius: var(--fc-radius); background: var(--fc-surface); }
  .fc-qr-logo-current .fc-check { margin: 0; }

  .fc-qr-payload { margin-top: 1rem; font-size: var(--fc-fs-sm); }
  .fc-qr-payload summary { cursor: pointer; color: var(--fc-text-muted); }
  .fc-qr-payload pre { margin: .5rem 0 0; padding: .6rem .8rem; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--fc-surface); border: 1px solid var(--fc-border); border-radius: var(--fc-radius); }
}

/* ARCHIVED OR DELETED IN PLACE, LEAVING. A row archived from a search
   that hides archived items, or deleted, stays two seconds as the
   confirmation — tinted, its status saying so — fading as it goes, then
   items.js takes it out. An archived row stays clickable (its button is
   Unarchive by now, and pressing it keeps the row); a deleted one is
   inert. The Undo toast below brings either back for ten seconds. */
@layer components {
  .fc-item.fc-item-leaving {
    outline: 2px solid var(--fc-ok-border);
    outline-offset: -2px;
    background: var(--fc-ok-bg);
    animation: fc-item-leave 2s ease-in forwards;
  }
  .fc-item.fc-item-leaving .fc-row-status { color: var(--fc-ok-text); font-weight: 600; }
  .fc-item.fc-item-leaving.fc-item-deleted { outline-color: var(--fc-danger); background: var(--fc-danger-bg); }
  .fc-item.fc-item-leaving.fc-item-deleted .fc-row-status { color: var(--fc-danger-text); }

  @keyframes fc-item-leave {
    0%, 55% { opacity: 1; }
    100% { opacity: .3; }
  }

  @media (prefers-reduced-motion: reduce) {
    .fc-item.fc-item-leaving { animation: none; }
  }
}

/* UNDO TOAST. One at a time, bottom centre, ten seconds after an archive
   or a delete; items.js builds it once and toggles `hidden`. */
@layer components {
  .fc-undo-toast {
    position: fixed;
    left: 50%;
    bottom: 1.25rem;
    transform: translateX(-50%);
    z-index: 60;
    display: flex;
    align-items: center;
    gap: 1rem;
    max-width: calc(100vw - 2 * var(--fc-gutter));
    padding: .6rem .6rem .6rem 1rem;
    background: var(--fc-text);
    color: var(--fc-surface);
    border-radius: var(--fc-radius-lg);
    box-shadow: var(--fc-shadow-lg);
    font-size: var(--fc-fs-sm);
  }
  .fc-undo-toast[hidden] { display: none; }
  .fc-undo-button {
    min-height: var(--fc-tap);
    padding: 0 .9rem;
    border: 1px solid currentColor;
    border-radius: var(--fc-radius);
    background: transparent;
    color: inherit;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .fc-undo-button:hover { background: color-mix(in srgb, currentColor 15%, transparent); }
  .fc-undo-button:focus-visible { outline: 2px solid var(--fc-primary-ring); outline-offset: 2px; }
}

/* ── Secure messaging (/secure, /m/{id}) — spec 2026-10-03 §5.6 ─────────── */
@layer components {
  .fc-secure .fc-hint code { font-family: var(--fc-font-mono); }

  .fc-secure-compose,
  .fc-secure-success,
  .fc-secure-mine,
  .fc-secure-opened { margin-top: 1.25rem; }

  .fc-secure-compose .fc-textarea { min-height: 10rem; }

  .fc-secure-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: 1rem;
  }

  .fc-secure-success {
    padding: 1rem;
    border: 1px solid var(--fc-ok-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-ok-bg);
    color: var(--fc-ok-text);
  }
  .fc-secure-success h2 { margin-top: 0; }

  .fc-secure-linkrow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
  }
  .fc-secure-linkrow .fc-field-label { flex-basis: 100%; }
  .fc-secure-linkrow .fc-input { flex: 1 1 18rem; min-width: 0; font-family: var(--fc-font-mono); }

  .fc-secure-list { list-style: none; margin: 0; padding: 0; }

  .fc-secure-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem .75rem;
    padding: .55rem 0;
    border-bottom: 1px solid var(--fc-border);
  }
  .fc-secure-dates { color: var(--fc-text-muted); }

  .fc-secure-chip,
  .fc-secure-flag {
    display: inline-block;
    padding: .1rem .5rem;
    border-radius: 999px;
    font-size: .8rem;
    border: 1px solid var(--fc-border);
  }
  .fc-secure-chip-active  { background: var(--fc-ok-bg);     color: var(--fc-ok-text);     border-color: var(--fc-ok-border); }
  .fc-secure-chip-opened  { background: var(--fc-tag-bg);    color: var(--fc-tag-text); }
  .fc-secure-chip-expired { background: var(--fc-warn-bg);   color: var(--fc-warn-text);   border-color: var(--fc-warn-border); }
  .fc-secure-chip-locked  { background: var(--fc-danger-bg); color: var(--fc-danger-text); }
  .fc-secure-flag { color: var(--fc-text-muted); }

  /* Hover-only row action, as on item rows; always shown without hover. */
  .fc-secure-delete {
    margin-left: auto;
    opacity: 0;
    transition: opacity .12s ease-in-out;
  }
  .fc-secure-row:hover .fc-secure-delete,
  .fc-secure-delete:focus-visible,
  .fc-secure-delete[data-armed] { opacity: 1; }
  .fc-secure-delete[data-armed] { color: var(--fc-danger-text); border-color: var(--fc-danger); }
  @media (hover: none) { .fc-secure-delete { opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .fc-secure-delete { transition: none; } }

  .fc-secure-body {
    white-space: pre-wrap;
    word-break: break-word;
    padding: 1rem;
    border: 1px solid var(--fc-border);
    border-radius: var(--fc-radius-lg);
    background: var(--fc-surface);
    font-family: var(--fc-font-mono);
  }
  .fc-secure-files { padding-left: 1.25rem; }
}

/* ── Slim on a phone (2026-10-09) ────────────────────────────────────────
   Three fixes that only matter without a mouse or on a narrow screen. Kept
   after the Slim and Batch blocks so they win on source order.
   1. The action bar (~12 icons of --fc-tap each) is hidden behind a "⋯"
      toggle at the end of the line; a tap opens it on a line of its own,
      wrapped, and a second tap or a tap outside the row closes it
      (items.js toggleActions). An open confirm or postpone tray keeps it
      shown regardless. The toggle exists in every view, shown only here.
   2. Narrow screen: the title has the first line to itself, full width;
      tags, date and the toggle share the line under it.
   3. The title wraps in full instead of being cut with an ellipsis. Two-
      line rows get a little air and a hairline between them.
   4. The batch checkbox is a full --fc-tap target with a smaller drawn box.
      Pure CSS, no data: image (CSP). */
@layer components {
  .fc-item > .fc-actions-toggle { display: none; }

  @media (hover: none) {
    :where(.fc-item-list[data-view="slim"]) > .fc-item {
      grid-template-columns: minmax(0, 1fr) auto auto auto;
    }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-actions-toggle {
      display: inline-flex;
      grid-column: 4;
      grid-row: 1;
      justify-content: center;
      min-width: var(--fc-tap);
      padding: .1rem;
      border-color: transparent;
      background: transparent;
    }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-actions-toggle[aria-expanded="true"] { color: var(--fc-link); }

    .fc-item-list[data-view="slim"] > .fc-item:not([data-actions-open], :has(.fc-confirm:not([hidden]), .fc-postpone[open])) > .fc-item-actions {
      display: none;
    }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-item-actions {
      flex-wrap: wrap;
      margin-top: .3rem;
    }
  }

  @media (max-width: 46rem) {
    .fc-item-list[data-view="slim"] { gap: .4rem; }
    :where(.fc-item-list[data-view="slim"]) > .fc-item:not(:last-child) { border-bottom-color: var(--fc-border); }

    .fc-item-list[data-view="slim"] > .fc-item > .fc-item-header {
      grid-column: 1 / -1;
    }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-tag-list:not([data-editing]) {
      grid-column: 1 / 3;
      grid-row: 2;
      max-width: none;
    }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-item-meta { grid-row: 2; }
    .fc-item-list[data-view="slim"] > .fc-item > .fc-actions-toggle { grid-row: 2; }
    .fc-item-list[data-view="slim"] .fc-item-title {
      white-space: normal;
      overflow: visible;
      text-overflow: clip;
      overflow-wrap: anywhere;
    }
  }

  @media (pointer: coarse) {
    .fc-item-list[data-batch] > .fc-item { padding-left: var(--fc-tap); min-height: var(--fc-tap); }

    .fc-item-list[data-batch] > .fc-item > .fc-batch-check {
      appearance: none;
      -webkit-appearance: none;
      top: 0;
      left: 0;
      width: var(--fc-tap);
      height: var(--fc-tap);
      background: transparent;
      border: 0;
      cursor: pointer;
    }
    .fc-item-list[data-batch] > .fc-item > .fc-batch-check::before {
      content: "";
      position: absolute;
      top: 50%;
      left: 50%;
      width: 1.25rem;
      height: 1.25rem;
      translate: -50% -50%;
      border: 2px solid var(--fc-text-muted);
      border-radius: .25rem;
      background: var(--fc-surface);
    }
    .fc-item-list[data-batch] > .fc-item > .fc-batch-check:checked::before {
      border-color: var(--fc-link);
      background: var(--fc-link);
    }
    .fc-item-list[data-batch] > .fc-item > .fc-batch-check:checked::after {
      content: "";
      position: absolute;
      top: 50%;
      left: 50%;
      width: .35rem;
      height: .7rem;
      translate: -50% -60%;
      rotate: 45deg;
      border: solid var(--fc-surface);
      border-width: 0 2px 2px 0;
    }
  }
}

@layer components {
  /* ═══ THE DUE CALENDAR (calendar/index.phtml) ════════════════════════
     Mobile first: an AGENDA — one block per day that has entries, empty
     days hidden, each day labelled with its full date. From 54rem the same
     table is a 7-column month grid. Same markup either way, no JS. */
  .fc-cal-header { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin-block: 1rem; }
  .fc-cal-heading { margin: 0; font-size: 1.4rem; }
  .fc-cal-nav { display: flex; gap: .25rem; }
  .fc-cal-search { display: flex; gap: .25rem; flex: 1 1 16rem; }
  .fc-cal-search .fc-input { flex: 1; min-width: 0; }

  .fc-cal, .fc-cal tbody, .fc-cal tr, .fc-cal td { display: block; }
  .fc-cal { width: 100%; border-collapse: collapse; }
  .fc-cal thead { display: none; }
  .fc-cal-day { padding: .5rem 0; border-block-end: 1px solid var(--fc-border); }
  .fc-cal-day.fc-cal-empty { display: none; }
  .fc-cal-daylink { color: inherit; text-decoration: none; }
  .fc-cal-daynum { display: none; }
  .fc-cal-dayname { display: block; font-weight: 600; margin-block-end: .25rem; }
  .fc-cal-day[aria-current="date"] .fc-cal-dayname { color: var(--fc-accent); }
  .fc-cal-day[aria-current="date"] { border-inline-start: 4px solid var(--fc-accent); padding-inline-start: .5rem; }
  .fc-cal-list { list-style: none; margin: 0; padding: 0; }
  .fc-cal-entry { display: flex; align-items: center; gap: .25rem; padding: .15rem .25rem; border-radius: var(--fc-radius); }
  .fc-cal-link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; text-decoration: none; }
  .fc-cal-link:hover, .fc-cal-link:focus-visible { text-decoration: underline; }
  .fc-cal-time { font-variant-numeric: tabular-nums; color: var(--fc-text-muted); }
  .fc-cal-entry.duedatePast .fc-cal-title { color: var(--fc-danger); font-weight: 600; }
  .fc-cal-reschedule { flex: none; min-inline-size: var(--fc-tap, 2.75rem); min-block-size: var(--fc-tap, 2.75rem); border: 0; background: none; color: var(--fc-text-muted); cursor: pointer; }
  .fc-cal-more { display: inline-block; font-size: .85rem; margin-block-start: .15rem; }
  .fc-cal-entry[draggable="true"] { cursor: grab; }
  .fc-cal-entry.fc-cal-dragging { opacity: .5; }
  .fc-cal-day.fc-cal-drop { outline: 2px dashed var(--fc-accent); outline-offset: -2px; }

  @media (min-width: 54rem) {
    .fc-cal { display: table; table-layout: fixed; }
    .fc-cal thead { display: table-header-group; }
    .fc-cal tbody { display: table-row-group; }
    .fc-cal tr { display: table-row; }
    .fc-cal th { padding: .25rem; font-weight: 600; color: var(--fc-text-muted); text-align: start; }
    .fc-cal td, .fc-cal-day.fc-cal-empty { display: table-cell; }
    .fc-cal-day { vertical-align: top; height: 7rem; padding: .25rem; border: 1px solid var(--fc-border); }
    .fc-cal-day { cursor: pointer; }
    .fc-cal-day:hover { background: color-mix(in srgb, var(--fc-accent) 5%, transparent); }
    .fc-cal-daylink:hover .fc-cal-daynum { text-decoration: underline; }
    .fc-cal-daynum { display: block; font-size: .85rem; }
    .fc-cal-dayname { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
    .fc-cal-day[aria-current="date"] .fc-cal-daynum { color: var(--fc-accent); font-weight: 700; }
    .fc-cal-day[aria-current="date"] { border-inline-start: 1px solid var(--fc-border); padding-inline-start: .25rem; background: color-mix(in srgb, var(--fc-accent) 10%, transparent); outline: 2px solid var(--fc-accent); outline-offset: -2px; }
    .fc-cal-entry { font-size: .85rem; }
    .fc-cal-reschedule { min-inline-size: 1.5rem; min-block-size: 1.5rem; opacity: 0; }
    .fc-cal-entry:hover .fc-cal-reschedule, .fc-cal-reschedule:focus-visible { opacity: 1; }
  }

  @media (pointer: coarse) {
    .fc-cal-reschedule { opacity: 1; }
  }
}
