    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    i[class^="ph"] { font-size: 1em; line-height: 1; display: inline-flex; }
    :root {
      --bg: #FAFBFC; --surface: #fff; --border: #d8e2f0;
      --text: #1a2235; --muted: #5b6e8a; --faint: #8fa3bb;
      --accent: #24A1E0; --accent2: #1d8fc8; --accent-bg: #e0f2fc;
      --danger: #E25057; --danger-bg: #fde4e4;
      --overlay: rgba(0,0,0,0.1);
      --r: 4px;
      --shadow: 0 1px 3px rgba(30,50,100,.07), 0 0 0 1px rgba(30,50,100,.05);
      --shadow-hover: 0 4px 16px rgba(30,50,100,.12), 0 0 0 1px rgba(30,50,100,.07);
    }
    /* App shell: header + scroll region as separate flex items, not page-level scroll.
       Intent: the browser's "scroll the focused input into view" behavior only ever
       scrolls the nearest scrollable ancestor (.scroll-region), and the header lives
       outside it entirely, so it can't be dragged along by THAT mechanism.
       (2026-08-21: a follow-up attempt also hard-locked html/body with overflow:hidden +
       position:fixed on body, on the theory that the document root itself could still be
       scrolled. Reverted the same day — confirmed via a real device that it fixed nothing
       (keyboard still pushed the header) while breaking pull-to-refresh outright, since
       Chrome's pull-to-refresh gesture depends on the document being overscroll-capable.
       That's proof the actual mechanism is something else — see the live viewport debug
       readout while this is being investigated.
       2026-08-29: same lesson relearned the hard way — `overscroll-behavior: none` on
       html/body was added to stop a real bug (see .scroll-region below) and broke
       pull-to-refresh the same way, for the same underlying reason: it makes the
       document non-overscroll-capable, which is exactly what that gesture needs. html/
       body must stay untouched; contain the effect at .scroll-region itself instead.) */
    body {
      font-family: 'Figtree', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500;
      background: var(--bg); color: var(--text); min-height: 100vh; min-height: 100dvh;
    }
    .wrap {
      display: flex; flex-direction: column;
      height: 100vh; height: 100dvh;
      overflow: hidden;
    }
    /* overflow-x: hidden (2026-09-04) — a real bug, not theoretical: CSS's own
       mutual-overflow rule silently promotes overflow-x from its default `visible` to
       `auto` whenever overflow-y is set to auto/scroll/hidden, so this region was
       *already* horizontally scrollable even though nothing here ever asked for that.
       A task whose text runs right up to the edge plus the small has-detail/has-repeat
       dot (styles.css's own negative margin-right trick to tuck it in tight) was enough
       stray width to make the whole region draggable sideways — reported live, one
       specific task in "Holger Feedback". `overflow-x: hidden` on the *x* axis only,
       explicitly — html/body must stay untouched for pull-to-refresh, per the hard
       lesson already documented above; this is the y-axis's own sibling problem, fixed
       the same contained way. */
    .scroll-region { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain; }
    .scroll-inner { max-width: 720px; margin: 0 auto; padding: 0 0 120px; }

    .top-bar {
      flex-shrink: 0;
      background: #FAFBFC;
      border-bottom: 1px solid rgba(0,0,0,.05);
    }
    .top-bar-inner {
      max-width: 720px; margin: 0 auto;
      display: flex; align-items: center; height: 64px;
      position: relative; padding: 0 16px 0 16px;
      /* space-between added 2026-09-11 (header-consolidation) — Due Dates/Focus Mode now
         append buildAppHeader's own back-button/menu-button directly here instead of via
         the .top-bar-left/.top-bar-right wrappers, so this alone pushes them to opposite
         edges the same way those wrappers used to. Safe for the default/wordmark view
         too: .top-bar-left's own flex:1 there already claims all remaining space
         regardless of the parent's justify-content. See .dlg-fullscreen .dlg-header
         below — kept in sync with this rule now that both are built by buildAppHeader. */
      justify-content: space-between;
    }
    .top-bar-left  { display: flex; align-items: center; gap: 4px; flex: 1; }
    .top-bar-right { display: flex; align-items: center; gap: 0; justify-content: flex-end; }
    /* Shared back arrow for every buildAppHeader (top bar + dialog), 2026-09-11
       header-consolidation. Both contexts use the exact same .icon-btn-top, but the -6px
       (top bar) vs -9px (dialog) margins below aren't leftover duplication to collapse
       into one number — each was independently measured (getBoundingClientRect) against
       its own context's checkbox column and landed on a different exact pixel match, so
       this stays two scoped values under one shared class rather than risk reintroducing
       the alignment bug either fix already solved. */
    .app-header-back { flex-shrink: 0; }
    .top-bar-inner .app-header-back { margin-left: -6px; }
    .dlg-fullscreen .dlg-header .app-header-back { margin-left: -9px; }
    /* True-centered icon+title+count group, 2026-09-11 header-consolidation — absolutely
       positioned against its container's own center regardless of what the back
       button/menu button on either side add up to (was inline cssText on a one-off
       Focus-Mode-only "headerCenter" div before; now shared by every buildAppHeader). */
    .app-header-center {
      position: absolute; left: 50%; transform: translateX(-50%);
      display: flex; align-items: center; gap: 8px;
      pointer-events: none; white-space: nowrap; overflow: hidden; max-width: 70%;
    }

    /* responsive header — hide "New task" label text on very narrow screens */
    @media (max-width: 399px) {
      .drag-new-label { display: none !important; }
      .drag-new-btn { padding: 5px 9px; }
    }
    h1 { font-size: 1.1rem; font-weight: 700; letter-spacing: -.02em; flex-shrink: 0; }
    .drag-new-btn {
      display: flex; align-items: center; gap: 6px;
      padding: 0 16px; height: 32px; border-radius: 8px;
      border: 1.5px dashed var(--accent-bg); background: transparent;
      font-size: .84rem; color: var(--accent); cursor: pointer;
      user-select: none; transition: all .15s; white-space: nowrap;
      font-family: inherit;
    }
    .drag-new-btn:hover { background: var(--accent-bg); border-color: var(--accent); }
    .drag-new-btn:active { opacity: .8; }
    .drag-new-btn.dragging { opacity: .35; }
    .top-actions { display: flex; gap: 8px; flex-shrink: 0; }
    .ws-label {
      font-size: .68rem; font-weight: 600; color: var(--muted);
      text-align: center; white-space: nowrap; overflow: hidden;
      text-overflow: ellipsis; max-width: 44px; letter-spacing: .01em;
    }
    .menu-divider {
      height: 1px; background: var(--border); margin: 4px 0;
    }
    .menu-section-label {
      font-size: .72rem; font-weight: 600; color: var(--faint);
      padding: 6px 14px 2px; letter-spacing: .04em; text-transform: uppercase;
    }
    #save-corner {
      position: fixed; top: 0; right: 0; z-index: 60;
      width: 0; height: 0; pointer-events: none;
      border-style: solid; border-width: 0 8px 8px 0;
      border-color: transparent #16a34a transparent transparent;
      opacity: 0; transition: opacity .35s ease;
    }
    #save-corner.visible { opacity: 0.5; }

    /* ─── auth overlay / landing page (2026-09-06) ─────────────────────────
       Was a small centered modal on an otherwise blank page — logged-out
       visitors saw nothing but "Sprinkle Do / Sign in to continue." Malte
       wanted it more appealing for people he's actually inviting now, plus
       a footer home for the privacy policy link Play Store distribution
       needs anyway. Now a real scrollable page: hero (the login/register/
       reset card itself is unchanged — showAuthOverlay's draw() just
       targets #auth-card-slot instead of owning the whole overlay) + a
       feature showcase + a footer. */
    #auth-overlay {
      position: fixed; inset: 0; z-index: 500;
      background: var(--bg);
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
    }
    .landing-hero {
      position: relative; /* containing block for .landing-sprinkles below */
      display: flex; flex-direction: column; align-items: center; text-align: center;
      padding: 56px 24px 40px;
      overflow: hidden; /* a dot rotated near an edge shouldn't force horizontal scroll */
    }
    .landing-wordmark { margin-bottom: 14px; position: relative; z-index: 1; }
    .landing-wordmark svg { display: block; max-width: 100%; height: auto; }
    .landing-tagline { font-size: 1.2rem; font-weight: 700; color: var(--text); max-width: 480px; margin-bottom: 8px; position: relative; z-index: 1; }
    .landing-subline { font-size: .92rem; color: var(--muted); max-width: 440px; margin-bottom: 36px; line-height: 1.5; position: relative; z-index: 1; }
    #auth-card-slot { width: 100%; max-width: 320px; position: relative; z-index: 1; }

    /* Real sprinkles (2026-09-06) — Malte, on the first landing-page pass: "Actual
       sprinkles across the page to make it look more friendly?" Small rotated capsule
       shapes in the same section-color palette every section itself already picks from
       — not the wordmark's own artwork (that's one fixed illustration, not built to be
       cut apart and rescattered), but genuinely the same "little colored jimmies"
       reading as this app's actual visual language, not stock confetti-graphic clipart.
       Positioned/rotated via inline style per dot (see sprinkleDotsHtml() in app.js);
       this class only supplies the capsule shape itself. */
    .landing-sprinkles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
    .sprinkle-dot { position: absolute; width: 14px; height: 5px; border-radius: 3px; }

    .landing-features {
      max-width: 920px; margin: 0 auto;
      display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
      gap: 18px; padding: 0 24px 56px;
    }
    .feature-card {
      position: relative; overflow: hidden;
      background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
      padding: 20px; border-top: 3px solid var(--fc-color, var(--accent));
    }
    .feature-card-icon {
      width: 38px; height: 38px; border-radius: 10px;
      display: flex; align-items: center; justify-content: center;
      background: var(--fc-bg, var(--accent-bg)); color: var(--fc-color, var(--accent));
      font-size: 1.25rem; margin-bottom: 12px;
      position: relative; z-index: 1;
    }
    .feature-card-title { font-weight: 700; font-size: .96rem; margin-bottom: 4px; position: relative; z-index: 1; }
    .feature-card-desc { font-size: .84rem; color: var(--muted); line-height: 1.5; position: relative; z-index: 1; }
    /* Two small corner sprinkles per card, in the card's own accent color — echoes the
       hero's sprinkles at a much quieter volume, tucked behind the real content. */
    .feature-card .sprinkle-dot { opacity: .35; }

    .landing-footer {
      text-align: center; padding: 24px; font-size: .8rem; color: var(--faint);
      border-top: 1px solid var(--border);
    }
    .landing-footer a { color: var(--muted); text-decoration: underline; }
    .landing-footer a:hover { color: var(--accent); }

    /* ─── auth card (the login/register/reset form itself) ────────────── */
    .auth-card {
      width: 100%; max-width: 320px;
      background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
      box-shadow: var(--shadow); padding: 28px 24px;
    }
    .auth-title { font-family: 'Outfit', sans-serif; font-weight: 800; font-size: 1.3rem; margin-bottom: 4px; }
    .auth-sub { color: var(--muted); font-size: .88rem; margin-bottom: 20px; }
    .auth-field { margin-bottom: 12px; }
    .auth-field label { display: block; font-size: .78rem; color: var(--muted); margin-bottom: 4px; }
    .auth-field input {
      width: 100%; padding: 9px 10px; border: 1px solid var(--border); border-radius: 6px;
      font: inherit; font-size: .92rem; background: var(--bg); color: var(--text);
    }
    .auth-field input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
    .auth-btn {
      width: 100%; padding: 10px; border: none; border-radius: 6px; margin-top: 6px;
      background: var(--accent); color: #fff; font: inherit; font-weight: 600; font-size: .92rem;
      cursor: pointer;
    }
    .auth-btn:hover { background: var(--accent2); }
    .auth-btn:disabled { opacity: .6; cursor: default; }
    .auth-error {
      background: var(--danger-bg); color: var(--danger); border-radius: 6px;
      padding: 8px 10px; font-size: .82rem; margin-bottom: 12px;
    }
    .auth-info {
      background: var(--accent-bg); color: var(--accent2); border-radius: 6px;
      padding: 8px 10px; font-size: .82rem; margin-bottom: 12px;
    }
    .auth-switch { text-align: center; margin-top: 14px; font-size: .82rem; color: var(--muted); }
    .auth-switch a { color: var(--accent); cursor: pointer; text-decoration: none; }
    .auth-switch a:hover { text-decoration: underline; }

    /* Sits right after .sec-count now (2026-08-29, was flush against the header's far
       right edge) — .sec-title-group's own flex gap handles the spacing, no margin
       needed here anymore. */
    .sec-shared-icon {
      display: inline-flex; align-items: center; justify-content: center;
      flex-shrink: 0; font-size: 1.2em; margin-top: 2px;
    }

    #share-modal {
      position: fixed; inset: 0; z-index: 400;
      background: var(--overlay);
      display: flex; align-items: center; justify-content: center;
      padding: 24px;
    }
    .share-card {
      width: 100%; max-width: 340px;
      background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
      box-shadow: var(--shadow-hover); padding: 22px 20px;
    }
    .share-title { font-family: 'Outfit', sans-serif; font-weight: 800; font-size: 1.1rem; margin-bottom: 4px; }
    .share-owner { color: var(--muted); font-size: .82rem; margin-bottom: 14px; }
    .share-members { margin-bottom: 14px; }
    .share-member-row {
      display: flex; align-items: center; justify-content: space-between;
      padding: 6px 2px; font-size: .88rem; border-bottom: 1px solid var(--border);
    }
    .share-member-row:last-child { border-bottom: none; }
    .share-pending-icon { color: var(--faint); font-size: .85rem; margin-left: 5px; vertical-align: -1px; }
    .share-remove-btn {
      background: none; border: none; cursor: pointer; color: var(--faint);
      width: 24px; height: 24px; border-radius: 5px; display: flex; align-items: center; justify-content: center;
    }
    .share-remove-btn:hover { background: var(--danger-bg); color: var(--danger); }
    .share-empty { color: var(--faint); font-size: .85rem; padding: 4px 2px; }
    .share-add-form { display: flex; gap: 6px; margin-bottom: 8px; }
    .share-add-form input {
      flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px;
      font: inherit; font-size: .85rem; background: var(--bg); color: var(--text);
    }
    .share-add-form .auth-btn { width: auto; margin-top: 0; padding: 8px 14px; white-space: nowrap; }
    .share-close-btn {
      width: 100%; padding: 9px; border: 1px solid var(--border); border-radius: 6px; margin-top: 6px;
      background: none; color: var(--text); font: inherit; font-size: .85rem; cursor: pointer;
    }
    .share-close-btn:hover { background: var(--bg); }

    .invite-result { margin-bottom: 10px; }
    .invite-status { font-size: .82rem; color: var(--muted); margin-bottom: 6px; }
    .invite-link-box {
      width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px;
      font: inherit; font-size: .8rem; background: var(--bg); color: var(--text); margin-bottom: 6px;
    }

    #server-error-bar {
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
      background: #E25057; color: #fff;
      display: flex; flex-direction: column;
      transform: translateY(100%); transition: transform .25s ease;
      border-top: 2px solid rgba(0,0,0,.15);
    }
    #server-error-bar.visible { transform: translateY(0); }
    #sync-success-bar {
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
      background: #2a7d4f; color: #fff;
      display: flex; flex-direction: column;
      transform: translateY(100%); transition: transform .25s ease;
      border-top: 2px solid rgba(0,0,0,.12);
    }
    #sync-success-bar.visible { transform: translateY(0); }
    #sync-success-bar .seb-changes { border-top-color: rgba(255,255,255,.15); }
    .seb-changes-visible { display: block; }
    .seb-main { display: flex; flex-direction: column; gap: 8px; padding: 11px 14px; font-size: .84rem; line-height: 1.4; }
    .seb-top { display: flex; align-items: flex-start; gap: 9px; }
    .seb-top i { font-size: 20px; flex-shrink: 0; opacity: .9; margin-top: 1px; }
    .seb-top .seb-msg strong { display: block; font-size: .88rem; margin-bottom: 1px; }
    .seb-top .seb-msg span { opacity: .8; font-size: .78rem; }
    .seb-btns { display: flex; gap: 7px; }
    .seb-btns button { flex: 1; }
    #server-error-bar button {
      background: rgba(0,0,0,.15); border: 1px solid rgba(0,0,0,.2);
      color: #fff; padding: 7px 10px; border-radius: 5px; font-size: .8rem;
      cursor: pointer; white-space: nowrap; transition: background .15s;
    }
    #server-error-bar button:hover { background: rgba(0,0,0,.25); }
    @media (min-width: 560px) {
      .seb-main { flex-direction: row; align-items: center; gap: 10px; padding: 11px 16px; }
      .seb-top { flex: 1; }
      .seb-btns { flex-shrink: 0; }
      .seb-btns button { flex: none; }
    }

    #share-invite-bar {
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
      background: #6270B6; color: #fff;
      display: flex; flex-direction: column;
      transform: translateY(100%); transition: transform .25s ease;
      border-top: 2px solid rgba(0,0,0,.12);
    }
    #share-invite-bar.visible { transform: translateY(0); }
    #share-invite-bar button {
      background: rgba(0,0,0,.15); border: 1px solid rgba(0,0,0,.2);
      color: #fff; padding: 7px 10px; border-radius: 5px; font-size: .8rem;
      cursor: pointer; white-space: nowrap; transition: background .15s;
    }
    #share-invite-bar button:hover { background: rgba(0,0,0,.25); }
    #share-invite-bar .sib-decline { background: none; border-color: rgba(255,255,255,.35); }
    #share-invite-bar .sib-decline:hover { background: rgba(0,0,0,.15); }
    #share-invite-bar .sib-more { opacity: .75; font-size: .74rem; margin-top: 2px; }

    /* "Enable notifications" bar (renderPushPermissionBar, 2026-09-13) — same fixed
       bottom-bar shape as #share-invite-bar above, just its own color so it doesn't read
       as an error (red) or an invite (purple) — a calm, informational teal. */
    #push-permission-bar {
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
      background: #2F8F9D; color: #fff;
      display: flex; flex-direction: column;
      transform: translateY(100%); transition: transform .25s ease;
      border-top: 2px solid rgba(0,0,0,.12);
    }
    #push-permission-bar.visible { transform: translateY(0); }
    #push-permission-bar button {
      background: rgba(0,0,0,.15); border: 1px solid rgba(0,0,0,.2);
      color: #fff; padding: 7px 10px; border-radius: 5px; font-size: .8rem;
      cursor: pointer; white-space: nowrap; transition: background .15s;
    }
    #push-permission-bar button:hover { background: rgba(0,0,0,.25); }
    #push-permission-bar .sib-decline { background: none; border-color: rgba(255,255,255,.35); }
    #push-permission-bar .sib-decline:hover { background: rgba(0,0,0,.15); }
    #push-permission-bar button:disabled { opacity: .6; cursor: default; }

    .seb-changes {
      border-top: 1px solid rgba(0,0,0,.15);
      padding: 8px 14px 10px; max-height: 160px; overflow-y: auto;
    }
    .seb-change {
      display: flex; align-items: baseline; gap: 8px;
      padding: 2px 0; font-size: .78rem; line-height: 1.5;
    }
    .seb-change-badge {
      font-size: .68rem; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
      padding: 1px 5px; border-radius: 3px; flex-shrink: 0; opacity: .9;
    }
    .seb-change.added .seb-change-badge    { background: rgba(134,239,172,.25); color: #86efac; }
    .seb-change.removed .seb-change-badge  { background: rgba(252,165,165,.2);  color: #fca5a5; }
    .seb-change.completed .seb-change-badge{ background: rgba(110,231,183,.2);  color: #6ee7b7; }
    .seb-change.uncompleted .seb-change-badge{ background: rgba(253,230,138,.2); color: #fde68a; }
    .seb-change.modified .seb-change-badge { background: rgba(253,230,138,.2);  color: #fde68a; }
    .seb-change-label { opacity: .85; }
    .seb-change-sec { opacity: .45; font-size: .72rem; margin-left: 2px; }

    #server-error-overlay {
      position: fixed; inset: 0; z-index: 199;
      background: rgba(0,0,0,.45);
      display: flex; align-items: center; justify-content: center;
      padding: 20px; opacity: 0; transition: opacity .2s ease; pointer-events: none;
    }
    #server-error-overlay.visible { opacity: 1; pointer-events: auto; }
    .seo-card {
      background: var(--surface); color: var(--text);
      border-radius: 12px; padding: 20px 18px;
      width: calc(100% - 32px); max-width: 320px;
      text-align: center; border: 1px solid var(--border);
      box-shadow: 0 8px 32px rgba(0,0,0,.25);
      max-height: calc(100dvh - 100px); overflow-y: auto;
    }
    .seo-card i { font-size: 30px; color: #E25057; display: block; margin-bottom: 10px; }
    .seo-card h3 { font-size: .95rem; font-weight: 600; margin-bottom: 6px; }
    .seo-card p { font-size: .79rem; color: var(--faint); line-height: 1.5; margin-bottom: 12px; }
    .seo-changelist {
      max-height: 120px; overflow-y: auto; margin-bottom: 12px;
      border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px;
      background: var(--bg); text-align: left;
    }
    .seo-change { display: flex; align-items: baseline; gap: 7px; padding: 2px 0; font-size: .78rem; }
    .seo-change .seb-change-badge { flex-shrink: 0; }
    .seo-change .seb-change-label { color: var(--text); opacity: .85; }
    .seo-unknown, .seo-nochanges {
      font-size: .78rem; color: var(--faint); margin-bottom: 12px;
      padding: 7px 8px; border-radius: 6px; background: var(--bg); border: 1px solid var(--border);
    }
    .seo-card .seo-btns { display: flex; flex-direction: column; gap: 6px; }
    .seo-card button {
      width: 100%; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border);
      background: var(--bg); color: var(--faint); font-size: .8rem; cursor: pointer;
      font-family: inherit; transition: background .15s, color .15s;
    }
    .seo-card button:hover { background: var(--hover); color: var(--text); }
    .seo-card button#seo-retry-top {
      background: #E25057; color: #fff; border-color: transparent; font-weight: 600;
    }
    .seo-card button#seo-retry-top:hover { background: #c93d43; }

    /* buttons */
    .btn {
      padding: 6px 12px; border-radius: var(--r); border: 1px solid var(--border);
      background: var(--surface); font-size: .82rem; cursor: pointer; color: var(--text);
      transition: all .12s; white-space: nowrap;
    }
    .btn:hover { background: #f0f4fa; border-color: #b0bfce; }
    .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
    .btn-primary:hover { background: var(--accent2); border-color: var(--accent2); }
    .icon-btn {
      background: none; border: none; cursor: pointer; color: var(--faint);
      font-size: .85rem; width: 28px; height: 28px; padding: 0;
      border-radius: 6px; transition: all .12s;
      display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    }
    .icon-btn:hover { color: var(--text); background: rgba(0,0,0,.08); }
    .icon-btn.danger:hover { color: var(--danger); background: var(--danger-bg); }
    .icon-btn.active { color: var(--accent); background: var(--accent-bg); }
    .icon-btn.active:hover { background: rgba(98,112,182,.18); }
    .icon-btn-top {
      background: none; border: none; cursor: pointer;
      color: rgb(148,163,184); font-size: 1.25rem; width: 48px; height: 48px;
      border-radius: 50%; display: flex; align-items: center; justify-content: center;
      transition: background .12s, color .12s; position: relative; flex-shrink: 0;
    }
    .icon-btn-top:hover { background: rgba(0,0,0,.07); color: var(--text); }
    .icon-btn-top:disabled { opacity: .35; cursor: default; pointer-events: none; }
    /* scale down for precise pointer (mouse/trackpad) */
    @media (pointer: fine) {
      .icon-btn-top { font-size: 1rem; width: 34px; height: 34px; }
      .top-bar-inner { height: 56px; }
    }
    /* Mobile task action bar (2026-09-14, floating-pill follow-up) — replaces the "⋯"
       corner menu on touch for ordinary tasks: a floating pill showing every relevant
       action as an icon (see updateTaskActionBar/buildTaskBarIcons/buildDraftBarIcons in
       app.js), positioned directly above whichever task/draft is currently selected
       (app.js's own positionTaskActionBar — `top` is set inline per-instance, not fixed
       here, since it tracks the selected row and moves on scroll). Malte, after the
       first flush-under-the-header version: "Make it floating with shadow and rounded
       corners - so it looks like a pill somehow." Pinned to the right edge (2026-09-14
       follow-up — Malte: "Move it to the right of the screen ... instead of the middle
       of the screen" — was `left:50%` + `translateX(-50%)`, centered), gap tightened
       from 16px to 8px the same day per a direct follow-up ("Only 8 px from the right
       border please"); `max-width` still leaves a matching real margin on the left too,
       so a wide icon set never touches that edge on a narrow phone. Brought to narrow
       desktop too (2026-09-15 follow-up) — Malte: "I love the new floating menu. Please
       introduce it to Desktop mode as well." (Wide desktop tried it the same day too,
       briefly — reverted after testing live: "remove the floating bar on the left for
       wide screens - doesn't make sense in that setup," since selecting a task there
       always opens the full dialog instead of expanding inline anyway, see renderTask's
       own triggerOpen.) Gating is JS-driven (updateTaskActionBar's own check), not CSS —
       the element is a singleton whose content/visibility/position are all recomputed
       on demand, same body-level-singleton precedent #wide-pane-empty already set for
       desktop wide mode. Shadow
       reuses the app's own --shadow-hover token (same elevation language as a hovered
       card) rather than a bespoke value. z-index:90 sits below .menu-drop's 100 (so a
       menu opened from a bar icon — e.g. the camera/library picker — draws above the
       bar) and well below every dialog/.task-drop's 10100+ range, but above ordinary
       scrolled task-row content. */
    .task-action-bar {
      display: none;
      position: fixed; right: 8px;
      z-index: 90;
      /* Dark, blurred glass look (2026-09-15 follow-up) — Malte gave the exact values:
         "background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(5px); And the icons in
         white please." Deliberately fixed/theme-independent (not `var(--surface)`
         any more) — this pill reads as its own dark overlay chrome regardless of
         light/dark mode, same idea as a native iOS/Android floating toolbar. Every
         icon/divider/active-state color below is overridden to match, since the
         default `.icon-btn-top` palette (mid-gray icon, light hover/active fills,
         `var(--border)` divider) was tuned for the light pill this used to be and
         would be too low-contrast — invisible, in the divider's case — against this
         dark backdrop. `-webkit-backdrop-filter` alongside the standard property for
         Safari/iOS, which still needs the prefix. */
      background: rgba(0, 0, 0, 0.5);
      backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
      border-radius: 999px;
      box-shadow: var(--shadow-hover);
      align-items: center; gap: 2px;
      padding: 4px 6px;
      max-width: calc(100vw - 32px);
    }
    /* No more tap-to-reveal "…" (2026-09-15 follow-up) — Malte, after trying that
       version for real: "Drop the '...' More functionality... The collapse one is
       fixed on the very right end. Below/behind that are the other options pin, move,
       expand - but the user has to scroll horizontally to reveal them," citing
       WhatsApp's own emoji-reaction bar (a horizontally-scrollable strip with a "+"
       pinned fixed at the end, never scrolling away) as the explicit reference. The
       bar itself (above) is no longer the scroll container — it's a plain flex row of
       exactly two children now: this strip (`flex:1 1 auto; min-width:0`, so it's the
       one that actually gets squeezed once the bar hits its own `max-width`) and
       `fixedIcon` (Close, a direct child of `.task-action-bar` — see
       `.task-action-bar > .icon-btn-top` below — `flex-shrink:0`, so it keeps its full
       size and never enters this scrollable strip at all, the WhatsApp "+" role).
       `overflow-x:auto` + the scrollbar-hiding rules moved here from the bar itself for
       exactly that reason — only this inner strip should ever scroll now. */
    .task-action-bar-scroll {
      display: flex; align-items: center; gap: 2px; min-width: 0;
      overflow-x: auto; overflow-y: hidden;
      -webkit-overflow-scrolling: touch;
      /* Scrolls via touch with no visible scrollbar (Malte: "if it gets too long it can
         be horizontally scrolled via touch without a scrollbar") — no existing pattern
         for this anywhere else in the app, both rules needed together for Firefox vs.
         WebKit/Blink. */
      scrollbar-width: none; -ms-overflow-style: none;
    }
    .task-action-bar-scroll::-webkit-scrollbar { display: none; }
    .task-action-bar-scroll .icon-btn-top { flex-shrink: 0; }
    /* Fades the strip's own left/right edge to transparent, live, based on real
       scroll position (2026-09-15 follow-up) — Malte, pointing at WhatsApp's own
       emoji-reaction bar: "They have a shadow or blurry edge that covers the half
       hidden item." A hard vertical clip at the cap (or at either scrolled edge) read
       as an abrupt cutoff; this softens it into the same dark backdrop instead, same
       visual language WhatsApp's own bar uses. `mask-image` (JS-set inline, see
       updateTaskActionBarFade in app.js — needs real scroll geometry, not expressible
       in CSS alone) rather than a gradient overlay element: masks the actual icons
       out smoothly without needing a separate positioned div layered on top guessing
       at the backdrop's own blurred/translucent color underneath. `-webkit-` alongside
       the standard property — Safari/iOS still needs the prefix for `mask-image`. */
    .task-action-bar-scroll { -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; }
    /* The fixed Close/"+"-role icon, a direct child of the bar itself rather than of
       the scroll strip above — see that rule's own comment. Round translucent-white
       background (2026-09-15 follow-up) — Malte, same message: "Maybe we could give
       the collapse icon a round background as well." Reads as its own separate,
       permanent control against the dark backdrop, same idea as WhatsApp's own "+"
       sitting in its own filled circle rather than blending into the scrollable row. */
    .task-action-bar > .icon-btn-top { flex-shrink: 0; background: rgba(255,255,255,.14); }
    .task-action-bar > .icon-btn-top:hover { background: rgba(255,255,255,.24); }
    .task-action-bar .icon-btn-top { color: #fff; }
    .task-action-bar-scroll .icon-btn-top:hover { background: rgba(255,255,255,.15); color: #fff; }
    /* Separates the "Add …" icons from the rest (2026-09-14 follow-up) — Malte's own
       requested order groups them first, then this divider, then Edit/Move/checkbox/Pin.
       Color bumped to a translucent white (2026-09-15) — `var(--border)` (a near-white
       light-mode gray) was effectively invisible against the new dark background. */
    .task-action-bar-divider { width: 1px; align-self: stretch; margin: 8px 2px; background: rgba(255,255,255,.25); flex-shrink: 0; }
    /* Hide/Show-checkbox icon's "currently on" state (2026-09-14 follow-up) — Malte:
       "this should become a rounded background filled, when selected." Originally the
       same color/background pairing .icon-btn.active uses elsewhere (accent-colored
       icon on a light accent-tinted background) — inverted (2026-09-15) to a filled
       white circle with an accent-colored icon, since the light `--accent-bg` tint
       barely registered against the new dark backdrop; a solid white fill reads the
       same "currently on" weight while actually staying visible. */
    .task-action-bar .icon-btn-top.active { color: var(--accent); background: #fff; }
    .menu-wrap { position: relative; }
    .menu-item-muted { opacity: .5; cursor: default; font-size: .78rem; }
    /* Plain non-clickable label row inside a .menu-drop (2026-09-15) — see
       openAppOptionsMenu's own `item.header` handling. Left-aligned to match
       .menu-item's own text start (14px padding there too), no hover/pointer since
       there's nothing to click. */
    .menu-item-header { padding: 8px 14px 2px; font-size: .72rem; color: var(--faint); }
    .touch-mode .menu-item-header { padding: 10px 18px 2px; font-size: .78rem; }

    /* overflow menu */
    .menu-drop {
      position: absolute; top: calc(100% + 6px); right: 0;
      background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r); box-shadow: 0 8px 28px rgba(0,0,0,.14);
      min-width: 160px; z-index: 100;
      max-height: calc(100vh - 20px); max-height: calc(100dvh - 20px); overflow-x: hidden; overflow-y: auto;
      /* Every dropdown lives outside .scroll-region (appended straight to document.body),
         so .scroll-region's own overscroll-behavior:contain never covered it — scrolling
         a long dropdown to its own top/bottom and continuing chained straight to the
         page underneath, which could drag the app header along with it (2026-08-30,
         reported specifically for the section header's own menu, but this is the shared
         class every dropdown in the app uses). */
      overscroll-behavior: contain;
    }
    .menu-item {
      display: flex; align-items: center; gap: 8px;
      padding: 9px 14px; font-size: .85rem; cursor: pointer;
      background: none; border: none; width: 100%; text-align: left;
      color: var(--text); transition: background .1s;
    }
    .menu-item:hover { background: #f0f4fa; }
    .menu-item:disabled { opacity: .32; cursor: default; pointer-events: none; }
    .menu-badge {
      position: absolute; top: 3px; right: 3px;
      min-width: 14px; height: 14px;
      background: #E25057; color: #fff;
      border-radius: 7px; font-size: .6rem; font-weight: 700;
      display: flex; align-items: center; justify-content: center;
      padding: 0 3px; pointer-events: none; line-height: 1;
    }
    .menu-item-badge {
      margin-left: auto;
      min-width: 18px; height: 18px;
      background: #E25057; color: #fff;
      border-radius: 9px; font-size: .7rem; font-weight: 700;
      display: flex; align-items: center; justify-content: center;
      padding: 0 4px; flex-shrink: 0;
    }
    .menu-item-count {
      margin-left: auto; flex-shrink: 0;
      font-size: .75rem; color: var(--faint);
      padding-left: 8px;
    }
    #quick-tip {
      position: fixed; z-index: 10001; pointer-events: none;
      background: #1a2235; color: #fff; font-size: .75rem;
      padding: 4px 9px; border-radius: 5px; white-space: nowrap;
      opacity: 0; transition: opacity .08s; box-shadow: 0 2px 6px rgba(0,0,0,.2);
    }
    #quick-tip.visible { opacity: 1; }
    .sprinkle-toast {
      position: fixed; left: 50%; bottom: 28px; z-index: 10001; pointer-events: none;
      transform: translate(-50%, 8px);
      background: var(--text); color: var(--bg);
      font-size: .82rem; font-weight: 600; white-space: nowrap;
      padding: 9px 18px; border-radius: 20px; box-shadow: 0 4px 16px rgba(0,0,0,.2);
      opacity: 0; transition: opacity .18s ease, transform .18s ease;
    }
    .sprinkle-toast.visible { opacity: 1; transform: translate(-50%, 0); }

    /* sections */
    .sections { display: flex; flex-direction: column; }
    .section { position: relative; margin-bottom: 36px; }
    .section.collapsed { margin-bottom: 10px; }
    .section.collapsed .sec-head {
      background: transparent;
      opacity: 0.5;
      transition: opacity 0.2s, background 0.2s;
    }
    .section.collapsed .sec-head:hover {
      background: var(--sec-bg, transparent);
      opacity: 1;
    }

    /* section header row */
    .sec-head {
      display: flex; align-items: center; gap: 8px;
      padding: 6px 4px 6px 16px; user-select: none;
      background: var(--sec-bg, #FAFBFC);
      height: 48px; transition: background .15s;
      position: sticky; top: 0; z-index: 12;
    }
    .sec-head:hover { background: var(--sec-hover, #f0f4f8); }

    .sec-title-group {
      flex: 1; display: flex; align-items: center;
      gap: 6px; min-width: 0; overflow: hidden; padding-right: 16px;
    }
    .sec-line { display: none; }
    .sec-title {
      font-family: 'Outfit', sans-serif;
      font-size: 24px; font-weight: 600; letter-spacing: -0.75px;
      color: var(--text); cursor: pointer;
      border-radius: 5px; padding: 2px 5px; margin: -2px -5px;
      min-width: 0; outline: none;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .sec-title-fixed { cursor: default; color: var(--muted); font-weight: 600; }
    [contenteditable]:empty::before {
      content: attr(data-ph); color: #aec0d2; pointer-events: none;
    }
    .sec-actions { display: flex; gap: 2px; opacity: 0; transition: opacity .15s; }
    .section:hover .sec-actions, .sec-actions:focus-within { opacity: 1; }
    /* Sized to exactly match .icon-btn-top (the app header's own "⋯") at rest, so the two
       read as the same control everywhere (2026-08-29). right:8px is deliberately nudged
       in from the 16px that would land it flush with .top-bar-inner's own padding.
       Vertically centered rather than pinned to the top, matching the header's
       flex-centered icon instead of just happening to fit a fixed-height box.
       2026-08-30: same icon and position on touch and desktop alike now, replacing the
       old corner-fold triangle entirely — desktop just starts at opacity:0 and reveals on
       hover/active, rather than showing a differently-shaped affordance than touch's
       permanently-visible one. Driven by `body.touch-mode`, NOT an `@media(pointer:fine)`
       hardware check — this app's own Switch-to-Touch/Desktop toggle is meant to fully
       override actual pointer capability (reported: switching the app to mobile view on
       a real desktop still only revealed this on hover, since a mouse is still a mouse
       hardware-wise; the toggle needs to win regardless of what's physically plugged in).
       .task-photo-thumb-btn mirrors this same body.touch-mode split so the two stay the
       same width in both states, not just at rest — see that rule for why the width had
       to match exactly, not just the right-edge position. border is transparent at rest so
       the box doesn't change size when .active gives it a real one (the "this menu is
       currently open" indicator). */
    .sec-more-btn {
      position: absolute; top: 50%; right: 8px; z-index: 2;
      transform: translateY(-50%);
      width: 48px; height: 48px; padding: 0;
      background: none; border: 1px solid transparent; border-radius: var(--r);
      cursor: pointer; opacity: 1;
      display: flex; align-items: center; justify-content: center;
      font-size: 1.25rem; line-height: 1;
      transition: opacity .15s, background .12s, border-color .12s;
    }
    body:not(.touch-mode) .sec-more-btn { width: 34px; height: 34px; font-size: 1rem; opacity: 0; }
    body:not(.touch-mode) .sec-head:hover .sec-more-btn,
    body:not(.touch-mode) .sec-more-btn:hover,
    body:not(.touch-mode) .sec-more-btn.active { opacity: 1; }
    /* :hover scoped to desktop only (2026-08-30) — on touch, a tap fires :hover with no
       real "pointer left" event to ever clear it, so the hover background just stayed
       stuck on-screen after tapping. .active alone (the real "menu is open" state, which
       *does* get explicitly cleared on close) still gets it on both. */
    body:not(.touch-mode) .sec-more-btn:hover { background: var(--sec-corner, rgba(0,0,0,.13)); }
    .sec-more-btn.active { background: var(--sec-corner, rgba(0,0,0,.13)); border-color: var(--sec-corner, var(--border)); }
    .sec-more-btn::after {
      /* Real (but invisible) paint layer purely to fix a mobile touch bug: without one,
         the first tap only registered as a hover, needing a second tap to actually click. */
      content: ''; position: absolute; inset: 0;
      background: #000; opacity: 0;
    }
    .sec-actions { display: none !important; }
    .sec-collapse {
      background: none; border: none; cursor: pointer; flex-shrink: 0;
      color: var(--faint); width: 28px; height: 28px; padding: 0;
      border-radius: 6px; line-height: 1;
      display: flex; align-items: center; justify-content: center;
      transition: color .12s, background .12s;
    }
    .sec-collapse:hover { color: var(--text); background: rgba(0,0,0,.07); }
    .sec-head:hover .sec-collapse { color: var(--text); background: rgba(0,0,0,.07); }
    .sec-collapse .arr { display: inline-flex; transition: transform .18s; }
    .sec-collapse.open .arr { transform: rotate(90deg); }
    .sec-count {
      font-family: 'Outfit', sans-serif;
      font-size: 24px; font-weight: 600; letter-spacing: -0.75px;
      color: var(--overlay);
      background: none;
      border-radius: 0;
      padding: 0;
      flex-shrink: 0;
      font-weight: 700;
    }

    /* drag handle */
    .handle {
      color: var(--faint); cursor: grab; font-size: .85rem;
      padding: 2px 5px; line-height: 1; border-radius: 5px;
      flex-shrink: 0; touch-action: none;
    }
    .handle.handle-fixed,
    .handle.handle-fixed:active {
      cursor: default;
    }
    .handle:active { cursor: grabbing; }
    .task-row .handle { display: none; }
    @media (pointer: fine) {
      .task-row .handle {
        display: flex; opacity: 0; transition: opacity .15s;
        position: absolute; left: -3px; top: 13px;
      }
      .task:hover .task-row .handle { opacity: 1; }
    }
    .sec-head .handle { opacity: 0; transition: opacity .15s; }
    .section:hover .sec-head .handle { opacity: 1; }

    /* tasks list */
    .tasks { display: flex; flex-direction: column; gap: 0; }

    /* task card */
    .task {
      position: relative;
      background: #FAFBFC;
      scroll-margin-top: 48px; /* matches .sec-head's sticky height — otherwise
        scrollIntoView({block:'start'}) lands a task flush with the top of
        #scroll-region, which the sticky section header then renders over. */
    }
    .task:hover { background: #f0f4f8; }
    /* A repeating task already done for its current cycle (2026-09-10, own dedicated row
       added same day — see renderDoneRepeatingTask's own comment in app.js) — Malte:
       "keep them in the list but maybe grey them out instead and show the next
       recurring date." Dimmed rather than disabled-looking, since the whole row is
       still a real, clickable thing (opens the full task dialog) — same reasoning
       task-tag-add's own disabled state dims to .35 rather than hiding outright. A
       little less dim on hover, so it still reads as a real row and not inert
       decoration. */
    .task.task-repeat-done { opacity: .55; }
    .task.task-repeat-done:hover { opacity: .8; }
    .task-repeat-done-row { display: flex; align-items: center; gap: 8px; padding: 11px 16px; }
    .task-repeat-done-row .task-text { flex: 1; min-width: 0; }
    .task-repeat-next-pill {
      font-size: .7rem; flex-shrink: 0; user-select: none;
      padding: 2px 8px; border-radius: 10px;
      background: rgba(91,110,138,.13); color: var(--muted);
    }
    /* Its own trailing block below the section's add-task affordance, not part of `.tasks`
       itself any more (2026-09-10 follow-up) — Malte: "bring the done repeating task
       below the '+ task' button." Collapsed by default, shown via the footer's own
       "• N Repeating" toggle (see .repeating-toggle below). A top/bottom border (Malte,
       same day: "give the repeating tasks maybe a top/bottom border") reads as a real
       separated block, not just more of the same list — no vertical padding of its own
       (removed same day, Malte: "remove padding from .tasks-repeat-done"), so the border
       sits flush against the row content above/below it instead of floating off it. */
    .tasks-repeat-done {
      margin-top: 6px;
      border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
    }
    /* A bit more breathing room above it on touch specifically (Malte: "On mobile - can
       you make the gap between repeating and the tasks below a few pixels more?") — the
       footer row it sits under is taller there too (see .touch-mode .section-footer's
       own padding/gap), so 6px reads tighter on mobile than the same value does next to
       desktop's more compact footer. */
    .touch-mode .tasks-repeat-done { margin-top: 14px; }
    /* A line between each done-repeating row, not just around the block as a whole (Malte,
       same day: "add a line between the repeating tasks") — same border-bottom/last-child
       pattern .repeating-list used to use, back when the Repeating-tasks dialog it
       belonged to still existed (removed 2026-09-10, see that rule's own former spot
       just below for why). */
    .tasks-repeat-done .task { border-bottom: 1px solid var(--border); }
    .tasks-repeat-done .task:last-child { border-bottom: none; }

    .task-row {
      display: flex; align-items: flex-start; gap: 8px;
      padding: 11px 16px 11px 16px;
      position: relative;
    }
    .task-check {
      appearance: none; -webkit-appearance: none;
      width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px;
      /* --task-check-accent (2026-09-11) — Malte: "color the checkboxes on the main app
         view... in the vibrant version of each section header." Set via a custom
         property, not border-color directly, specifically so :hover/:checked below can
         still override it normally — an inline border-color would always win over both
         regardless of specificity, silently killing the existing hover-darken feedback
         and the checked-state's own border-color for any section with a color set. */
      border: 2px solid var(--task-check-accent, var(--border)); border-radius: 6px;
      background: #fff; cursor: pointer; position: relative; transition: all .12s;
      box-shadow: 0 1px 3px rgba(0,0,0,.08), inset 0 1px 2px rgba(0,0,0,.04);
    }
    .task:hover .task-check { border-color: #7a9ab8; }
    .task-check:checked { background: var(--accent); border-color: var(--accent); }
    .task-check:checked::after {
      content: ''; position: absolute; left: 4px; top: 1px;
      width: 7px; height: 13px; border: 2px solid #fff;
      border-top: none; border-left: none; transform: rotate(45deg);
    }
    .task-text-wrap {
      flex: 1; display: flex; align-items: baseline; gap: 6px; min-width: 0;
    }
    .task-text {
      font-size: .9375rem; line-height: 1.5; color: #1d293d;
      word-break: break-word; outline: none; border-radius: 3px; min-width: 0;
      /* font-family: inherit (2026-09-12) — Malte: "font size (or even font) for adding
         inline new tasks looks too small... and different then when rendered." The
         rendered version of this class always sits on a plain <span>, which inherits the
         page's own loaded font (Figtree) with no help needed; the new-task draft card's
         version sits on an <input> instead (renderGap), and a bare <input> does NOT
         inherit font-family from its surroundings — the browser gives it its own UA
         default (confirmed live: plain Arial) regardless of what's declared here, unless
         told otherwise. Harmless on the <span> case (already inheriting Figtree either
         way), fixes the <input> case for real. font-weight (2026-09-12 follow-up) — Malte,
         after that fix: "is the font weight the same... it still feels slightly slimmer."
         Real, not just an input-rendering artifact — confirmed live, 500 rendered vs 400
         on the draft input. Same root cause exactly, just `body`'s own font-weight:500
         this time instead of Figtree: a <span> inherits it for free, a bare <input>
         doesn't. */
      font-family: inherit;
      font-weight: inherit;
    }
    /* No visible focus outline while editing (2026-09-12) — Malte: "remove the visible
       outline when editing task text and notes" (desktop and mobile both — this rule has
       no pointer-context split to begin with, so one change covers both). Was a soft
       custom ring replacing the browser's own harsh default one, not the default itself;
       removed outright rather than softened further, matching the same ask applied to
       .task-detail-title and textarea.note-view just below/elsewhere in this file. */
    .task-text:focus { outline: none; }

    .task-meta {
      display: flex; justify-content: space-between; align-items: center;
      padding: 5px 6px 0 16px;
    }
    .task-meta + .task-row { padding-top: 4px; }
    .task-due {
      font-size: .7rem; user-select: none; flex-shrink: 0;
      display: inline-flex; align-items: center;
      padding: 2px 8px; border-radius: 10px;
      background: var(--accent-bg); color: var(--accent);
    }
    .task-due.overdue { background: var(--danger-bg); color: var(--danger); }
    .task-due.due-today { background: #fff3e0; color: #FCAF32; }
    /* Due Dates page only (renderOverview's own addGroup) — see its own comment for why
       this exists alongside renderTask's ordinary (mostly-collapsed) .task-tags row.
       Left-padding matches .task-note-excerpt's own 44px, to sit under the title rather
       than the checkbox. */
    .task-overview-subtitle {
      display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
      padding: 0 16px 8px 44px; margin-top: -6px;
      cursor: pointer;
    }
    .task-note-excerpt {
      font-size: .78rem; color: #b8cad8; line-height: 1.5;
      padding: 0 16px 3px 44px; cursor: pointer;
      max-height: 1.5em; overflow: hidden;
      white-space: pre-wrap; word-break: break-word;
      -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
      mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
      margin-top: -10px;
    }
    /* note indicator dot */
    .task-note-dot {
      width: 6px; height: 6px; border-radius: 50%;
      background: rgba(98,112,182,.3); flex-shrink: 0; align-self: center; margin-left: 4px;
    }
    .task-detail-dot {
      width: 6px; height: 6px; border-radius: 50%;
      background: rgba(98,112,182,.2); display: inline-block;
      vertical-align: middle; margin-left: 4px; flex-shrink: 0;
    }
    /* has-overdue's own color variant dropped (2026-09-15) — Malte: "Remove the dot in
       general if there is a schedule - we now have that pill. So dot is now just a
       single indicator if there is a note or not." Overdue-ness now only ever shows on
       the schedule pill itself (.task-due.overdue below), not here too. */
    .task-text.has-detail::after {
      content: ''; display: inline-block;
      width: 6px; height: 6px; border-radius: 50%;
      vertical-align: middle; margin-left: 4px; margin-right: -10px;
      background: rgba(98,112,182,.2);
    }
    .task-progress {
      font-size: .73rem; font-weight: 600; color: var(--faint);
      background: var(--bg); border-radius: 20px;
      padding: 3px 7px; flex-shrink: 0; letter-spacing: 0; align-self: flex-start; margin-top: 3px;
    }

    /* task note area — part of the card, separated by a rule */
    .task-note-area {
      margin: -11px 8px 4px 8px;
      background: none; border-radius: var(--r);
      padding: 10px 10px 0px 10px;
      position: relative;
    }
    .task-list .task-note-area { margin-bottom: 0; }
    .task-list .task-tags { padding-bottom: 2px; }
    .task-tags {
      display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
      padding: 4px 0 8px 0;
    }
    .task-tag {
      font-size: .7rem; padding: 2px 8px; border-radius: 10px;
      display: inline-flex; align-items: center; gap: 4px;
      user-select: none; flex-shrink: 0;
    }
    .task-tag-del {
      cursor: pointer; opacity: .4; background: none; border: none;
      padding: 0; color: inherit; line-height: 1; display: flex; margin-left: 1px;
    }
    .task-tag-del:hover { opacity: 1; color: var(--danger); }
    .task-tag-del svg { width: 10px; height: 10px; }
    .task-tag-add {
      font-size: .7rem; padding: 2px 8px; border-radius: 10px;
      border: 1px dashed rgba(0,0,0,.2); color: var(--faint);
      cursor: pointer; background: none; font-family: inherit;
    }
    .task-tag-add:hover { background: rgba(0,0,0,.05); }
    /* Tags removed entirely 2026-09-10 — the tag picker portal (.tag-picker*), excerpt
       tag dots (.excerpt-tags/.excerpt-tag-dot), and the "Manage tags" dialog
       (.manage-tags-*) that used to live here are all gone. .task-tag/.task-tag-add/
       .task-tags themselves are NOT part of that removal — despite the name, they're the
       app's generic chip/add-button styling, still actively used for due date, repeat,
       photo-add, the Areas picker, and the Overview's own section-name badge.
       .agenda-overlay/.agenda-bar/.agenda-sec/.agenda-row below were already dead before
       this change (an older Overview/agenda layout, superseded by reusing .section/
       .sec-head — nothing in app.js references these) — left alone, unrelated to tags,
       not this change's job to clean up. */
    .agenda-overlay { position:fixed;inset:0;z-index:9000;background:var(--bg);overflow-y:auto;font-family:inherit; }
    .agenda-bar { position:sticky;top:0;background:var(--bg);display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);z-index:1; }
    .agenda-bar-title { font-size:.95rem;font-weight:700;color:var(--text); }
    .agenda-sec { padding:24px 20px 6px;display:flex;align-items:center;gap:8px; }
    .agenda-sec-label { font-size:.75rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em; }
    .agenda-row { display:flex;align-items:center;gap:10px;padding:10px 20px;border-bottom:1px solid var(--border);cursor:pointer; }
    .agenda-row:hover { background:var(--surface); }
    .agenda-row-title { flex:1;font-size:.88rem;color:var(--text); }
    .agenda-row-section { font-size:.7rem;color:var(--faint); }
    /* Real ⋮ icon, not a folded page-corner (2026-09-07) — Malte: "change the visual
       indicator to an actual icon... show an overflow three vertical dot menu when a
       task is clicked." Same show/hide triggers as before (still opt-in — task actively
       being edited, notes expanded, or the dropdown itself open), just a plain
       dots-three-vertical icon (ph('dots-three-vertical'), matching the identical icon
       already used for the section's own "⋯" menu) instead of a hand-drawn clip-path
       triangle. Now rendered unconditionally, including on a photo-bearing task — see
       .task-photo-thumb-btn's own --photo-shift rule just below for how the two share
       this same top-right corner without overlapping.
       Follow-up, same day: top:8px (was 4px) centers the icon on the checkbox's own
       vertical center (measured live — checkbox center sits 4px below where the button's
       box used to start), 1rem icon (was .95rem) matches the header's own "More options"
       icon exactly (measured live: 16px), and the tinted background is gone entirely —
       the icon's own color is the section's vibrant accent instead
       (--task-corner-vibrant), no fill box around it at all. Hover still gets a plain
       neutral tint, not a colored one — a hint that it's clickable, not a second use of
       the section's own accent color. */
    .task-row-overflow {
      position: absolute; top: 8px; right: 4px;
      width: 28px; height: 28px; padding: 0;
      display: flex; align-items: center; justify-content: center;
      background: none; border: none; border-radius: 6px; cursor: pointer;
      color: var(--task-corner-vibrant, var(--faint));
      opacity: 0; pointer-events: none;
      transition: opacity .18s ease, background .18s ease;
    }
    .task-row-overflow i { font-size: 1rem; }
    .task.expanded .task-row-overflow,
    .task.overflow-active .task-row-overflow,
    .task:has(.task-text[contenteditable="true"]) .task-row-overflow,
    .task-row-overflow.active {
      opacity: 1; pointer-events: auto;
    }
    .task-row-overflow:hover,
    .task-row-overflow.active {
      background: rgba(0,0,0,.08);
    }
    /* Section apps (2026-08-28) — metric tracker row. Collapsed row is deliberately the
       same height as an ordinary task's (.task-row's own 11px/16px padding, unchanged —
       just sized every child to fit the same ~20px content line the checkbox sets, not
       the slightly taller 24px controls this started with). Whole row is clickable to
       expand/collapse, same gesture as an ordinary task's notes — no dedicated chevron. */
    .metric-task .task-row, .bp-task .task-row { align-items: center; cursor: pointer; }
    /* Section-app row icon, shared across metric/medication/habit/potty-log (2026-09-11
       follow-up) — Malte: "the icons for section apps need to be bigger on mobile to be
       more consistent with the checkbox. Section app text also should be vertically
       aligned with the ordinary task text [on mobile]." Was four separate copies of the
       identical inline style (`flex-shrink:0;font-size:1.2rem;color:...`), color the
       only part that actually needs to be per-instance/JS-set — moved the rest here so
       touch mode can target it at all. Desktop value unchanged from the old inline style
       (Malte: "On desktop that is already correct") — confirmed via direct measurement
       before assuming so, not just carried over blind. `i.` prefix, not just
       `.task-app-icon` — the shared base rule `i[class^="ph"] { font-size: 1em; ...}` has
       equal (element+attribute) specificity to a bare class, so without it this would be
       silently overridden, the exact bug already found and fixed once this session for
       .task-detail-row-icon. See the touch-mode override's own comment for the mobile
       half of this fix. */
    i.task-app-icon { flex-shrink: 0; font-size: 1.2rem; }
    /* Matches .task-text exactly (2026-08-29) — was a hair smaller (.92rem vs .9375rem),
       enough to visibly stand out as "different" sitting in the same list as ordinary
       tasks. */
    .metric-task-name { font-size: .9375rem; line-height: 1.5; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-radius: 3px; }
    /* Same focus outline as .task-text (2026-08-29 — photo rows became click-to-rename in
       place, same as an ordinary task, so editing one should look the same too), including
       the same 3px border-radius (set on the base rule above, matching .task-text's own,
       since an outline follows its element's border-radius automatically — an unrounded
       element would otherwise draw a sharp-cornered outline no matter what the outline's
       own properties say). Overflow switches to visible/normal while editing — the
       ellipsis truncation that's right for a settled name would otherwise clip the outline
       itself and hide anything typed past the row's usual width. */
    .metric-task-name:focus {
      outline: 1px solid rgba(98,112,182,.2); outline-offset: 2px;
      overflow: visible; text-overflow: clip; white-space: normal;
    }
    .metric-task-value { font-size: .88rem; font-weight: 600; color: var(--muted); flex-shrink: 0; white-space: nowrap; }
    /* A draft value (adjusted but not yet Confirmed) reads as distinctly "not sent yet" */
    .metric-task-value.pending { color: var(--accent); }
    .metric-sparkline { flex-shrink: 0; display: flex; align-items: center; line-height: 0; }
    .metric-adjust-btn {
      flex-shrink: 0; font-size: .8rem; font-weight: 600; font-family: inherit;
      color: var(--muted); background: var(--bg); border: 1px solid var(--border);
      border-radius: 999px; width: 20px; height: 20px; line-height: 1; padding: 0;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
    }
    .metric-adjust-btn:hover { background: var(--border); color: var(--text); }
    .metric-confirm-btn {
      flex-shrink: 0; background: var(--accent); color: #fff; border: none;
      border-radius: 999px; width: 20px; height: 20px; padding: 0;
      display: flex; align-items: center; justify-content: center;
      cursor: pointer; font-size: .72rem;
    }
    .metric-confirm-btn:hover { background: var(--accent2); }
    .metric-chart { padding: 4px 16px 14px 16px; cursor: default; }
    .metric-chart-empty { font-size: .78rem; color: var(--faint); padding: 8px 0; }
    .metric-chart-scroll { overflow-x: auto; }
    /* Photo view dialog (2026-08-28, simplified 2026-09-02 to Close-only — see
       openPhotoDialog's own comment: Move/Remove photo/Delete moved into the task's own
       single "⋯" menu once photo stopped being a distinct task kind). */
    /* Still used elsewhere (a tracker-picker dialog's own "Back" button) even though the
       photo dialog's own edit button that originally motivated this class is gone. */
    .photo-icon-btn { flex-shrink: 0; background: none; border: none; cursor: pointer; color: var(--faint); padding: 2px; display: flex; }
    .photo-icon-btn:hover { color: var(--text); }

    /* Photo lightbox (2026-09-09) — Malte: "Just the photo is shown - background dimmed
       (similar to now). But the photo has no real dialog around. And the photo can be
       zoomed in and dragged... by finger gestures." Replaces the old centered-card
       .sec-edit-modal (title bar, white card, footer) with just the image on a dimmed
       backdrop — no chrome at all except a small floating close button, since a
       card/footer around a photo was exactly the "real dialog" feel being asked to drop.
       Still a real .sec-edit-overlay underneath (composed with this .lightbox modifier,
       not a separate class) so window.handleNativeBackPress()'s existing
       `.sec-edit-overlay, .modal-bg` query keeps closing it on system back for free, same
       reasoning as every other dialog shell this session. Darker than the base
       .sec-edit-overlay's own rgba(0,0,0,.25) — Malte said "similar to now," but a photo
       specifically wants a properly dark backdrop for the image itself to read against,
       the way every real lightbox/photo-viewer convention already does; close enough to
       "similar" that it wasn't worth a separate round-trip to ask. */
    .sec-edit-overlay.lightbox { background: rgba(0,0,0,.88); padding: 0; }
    .lightbox-imgwrap {
      width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
      overflow: hidden; touch-action: none;
    }
    .lightbox-img {
      max-width: calc(100% - 32px); max-height: calc(100% - 32px);
      object-fit: contain; touch-action: none; user-select: none; -webkit-user-drag: none;
      will-change: transform;
    }
    .lightbox-img.lightbox-snap { transition: transform .2s ease; }
    .lightbox-close {
      position: fixed; top: calc(12px + env(safe-area-inset-top)); right: 12px; z-index: 1;
      width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
      background: rgba(0,0,0,.4); color: #fff;
      display: flex; align-items: center; justify-content: center; font-size: 1.2rem;
    }
    .lightbox-close:hover { background: rgba(0,0,0,.6); }
    /* Mirrors .lightbox-close exactly, just the opposite top corner (2026-09-12) — Malte:
       "upper right corner for... the 'x' for closing. In the upper left corner add a
       trashcan icon for removing the picture." */
    .lightbox-delete {
      position: fixed; top: calc(12px + env(safe-area-inset-top)); left: 12px; z-index: 1;
      width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
      background: rgba(0,0,0,.4); color: #fff;
      display: flex; align-items: center; justify-content: center; font-size: 1.2rem;
    }
    .lightbox-delete:hover { background: rgba(0,0,0,.6); }
    .lightbox-empty { color: rgba(255,255,255,.7); font-size: .9rem; }
    /* Phrase task detail dialog (2026-09-02) — same modal shape as the photo dialog,
       text instead of an image: the phrase itself large (it's the whole point of opening
       this), pronunciation and translation below in a quieter, smaller style. */
    .phrase-dialog-phrase { font-size: 1.6rem; font-weight: 600; line-height: 1.35; margin: 4px 0 14px; word-break: break-word; }
    .phrase-dialog-pinyin { font-size: .95rem; color: var(--text); margin-bottom: 8px; font-style: italic; }
    .phrase-dialog-translation { font-size: .92rem; color: var(--text); }
    .phrase-dialog-label { font-style: normal; font-weight: 600; color: var(--faint); }
    /* Phrase task row (2026-09-02) — plain checkbox task row, just a leading translate
       icon and non-editable text; clicking the text opens openPhraseDialog instead of the
       inline edit mode an ordinary task's title would enter. */
    .phrase-task-text { line-height: 1.3; }
    /* Task row's own photo thumbnail (2026-09-02) — replaces the old dedicated photo row's
       pairing of a small leading thumbnail plus a separate trailing "view" button with one
       element: the thumbnail itself sits where that view button used to (absolutely
       positioned, right:8px, vertically centered — same spot .sec-more-btn's own column
       aligns to elsewhere), and *is* the click target, with a small expand-icon badge
       overlaid on it rather than a separate control beside it. Same 48x40 touch / 34x24
       desktop split .photo-view-btn used to have, kept for the same reason: matching
       .sec-more-btn's own two exact sizes so everything in this right-hand column still
       lines up, on both touch and desktop. */
    .task-photo-thumb-btn {
      position: absolute; top: 50%; right: 8px; z-index: 3;
      transform: translateY(-50%) translateX(var(--photo-shift, 0px));
      transition: transform .2s ease;
      width: 48px; height: 40px; padding: 0;
      border-radius: var(--r); border: 1px solid var(--border); background: var(--bg);
      cursor: pointer; overflow: hidden;
    }
    body:not(.touch-mode) .task-photo-thumb-btn { width: 34px; height: 24px; }
    /* Right inset matches the row's own right padding, not a fixed 8px, in touch mode
       specifically (2026-09-11 follow-up) — Malte: "Padding 16px also for ordinary task
       incl. photos," right after the same 16px landed on every other row's own right
       edge (.touch-mode .task-row above). Desktop stays at the shared 8px base — this
       thumbnail is what that value was originally measured *from* as the app's own
       baseline, so it has nothing to catch up to there. See .touch-mode .has-photo
       .task-text-wrap just below for the matching text-column reservation this moves
       out from under. */
    .touch-mode .task-photo-thumb-btn { right: 16px; }
    /* Slides left to clear the ⋮ menu icon's own space, exactly when that icon itself
       becomes visible — same three trigger conditions as .task-row-overflow's own
       opacity/pointer-events reveal above (a fourth, the dropdown's own .active state,
       is deliberately not repeated here — reaching that state requires the icon to
       already be clickable, which means one of these three is already true, so it adds
       no real coverage of its own). 32px (not 28, the icon's own box) — the icon sits
       inset 4px from the edge, so 28+4 covers its real footprint with a hair of
       breathing room. Clicking the thumbnail itself still only opens the photo (see its
       own onclick's stopPropagation in app.js) — this only reacts to the *task*
       becoming active, never fires from a photo click alone. */
    /* :not(.no-overflow-menu) on every trigger here (2026-09-13 follow-up) — Malte, on a
       shopping-list task (no overflow menu at all in that mode, see app.js's own comment
       on where .no-overflow-menu gets set): "the picture stays in the same position,
       since there is not more overflow menu to make space for." The third trigger
       (title mid-edit) is its own separate condition from "has an overflow menu to make
       room for" — nothing here previously excluded a task with no such menu from it. */
    .task.has-photo.expanded:not(.no-overflow-menu) .task-photo-thumb-btn,
    .task.has-photo.overflow-active:not(.no-overflow-menu) .task-photo-thumb-btn,
    .task.has-photo:not(.no-overflow-menu):has(.task-text[contenteditable="true"]) .task-photo-thumb-btn {
      --photo-shift: -32px;
    }
    /* Touch mode's own icon box is 34px + a 4px inset (38 total), not desktop's 28+4 —
       see .touch-mode .task-row-overflow's own comment for why the icon itself is
       bigger there. */
    .touch-mode .task.has-photo.expanded:not(.no-overflow-menu) .task-photo-thumb-btn,
    .touch-mode .task.has-photo.overflow-active:not(.no-overflow-menu) .task-photo-thumb-btn,
    .touch-mode .task.has-photo:not(.no-overflow-menu):has(.task-text[contenteditable="true"]) .task-photo-thumb-btn {
      --photo-shift: -38px;
    }
    .task-photo-thumb { display: block; width: 100%; height: 100%; object-fit: cover; }
    .task-photo-thumb-icon {
      position: absolute; right: 2px; bottom: 2px;
      width: 14px; height: 14px; border-radius: 50%;
      background: rgba(0,0,0,.45); color: #fff;
      display: flex; align-items: center; justify-content: center;
      font-size: 8px; pointer-events: none;
    }
    body:not(.touch-mode) .task-photo-thumb-icon { width: 12px; height: 12px; font-size: 7px; }
    /* Reserves the same space in the title's own column that .task-photo-thumb-btn takes
       up, same inset + width math the old .photo-task-name margin used, now applied to
       any ordinary task's title-wrap instead of a photo-specific one. Touch inset bumped
       48+16=64 (2026-09-11 follow-up, same "Padding 16px also for ordinary task incl.
       photos" pass as .touch-mode .task-photo-thumb-btn's own matching move) — desktop
       keeps its original 34+8=42, unchanged. */
    .has-photo .task-text-wrap { margin-right: 64px; }
    body:not(.touch-mode) .has-photo .task-text-wrap { margin-right: 42px; }
    /* Widened by the same amount the thumbnail itself shifts by when active (see
       --photo-shift above) — otherwise a long, wrapping title could run under the
       now-relocated thumbnail instead of stopping short of it. 64+38=102 touch,
       42+32=74 desktop — kept in sync with --photo-shift's own two values by hand,
       since margin-right can't read a custom property's numeric value directly. */
    /* :not(.no-overflow-menu) here too (2026-09-13 follow-up) — same reasoning as
       --photo-shift's own identical addition just above: no point reserving room for a
       shift that no longer happens. */
    .task.has-photo.expanded:not(.no-overflow-menu) .task-text-wrap,
    .task.has-photo.overflow-active:not(.no-overflow-menu) .task-text-wrap,
    .task.has-photo:not(.no-overflow-menu):has(.task-text[contenteditable="true"]) .task-text-wrap { margin-right: 102px; }
    body:not(.touch-mode) .task.has-photo.expanded:not(.no-overflow-menu) .task-text-wrap,
    body:not(.touch-mode) .task.has-photo.overflow-active:not(.no-overflow-menu) .task-text-wrap,
    body:not(.touch-mode) .task.has-photo:not(.no-overflow-menu):has(.task-text[contenteditable="true"]) .task-text-wrap { margin-right: 74px; }

    /* Inline schedule pill (2026-09-15) — Malte: "put the schedule tag inline similar to
       where pictures are shown on a task... at the very right side. if there is a picture
       attached it will live left to that picture." Mirrors .task-photo-thumb-btn's own
       corner exactly: same vertical center, same base right inset, sliding further right
       (not left, since it's not a fixed-size box like the thumb) whenever a photo is also
       present so the two never overlap. Reuses .task-due/.task-recurrence-pill for its
       actual look (color, padding, shape) — this class only adds the positioning. */
    .task-schedule-pill {
      position: absolute; top: 50%; right: 8px; z-index: 2;
      transform: translateY(-50%) translateX(var(--sched-shift, 0px));
      transition: transform .2s ease;
      white-space: nowrap;
    }
    .touch-mode .task-schedule-pill { right: 16px; }
    /* Sits left of the photo thumbnail once both exist — clears the thumbnail's own base
       box (34px desktop / 48px touch) plus its own right inset, plus a small gap. */
    .task.has-photo .task-schedule-pill { right: 48px; }
    .touch-mode .task.has-photo .task-schedule-pill { right: 72px; }
    /* Same corner-icon clearance --photo-shift already handles for the thumbnail, needed
       here too only for the no-photo case — with a photo present the pill's own 48/72px
       base offset already clears the overflow icon's small top-right box on its own.
       Touch mode never shows that icon at all for an ordinary task any more (see
       app.js's own !touchMode gate on menuBtn), so no touch variant needed here. */
    .task.has-schedule:not(.has-photo).expanded:not(.no-overflow-menu) .task-schedule-pill,
    .task.has-schedule:not(.has-photo).overflow-active:not(.no-overflow-menu) .task-schedule-pill,
    .task.has-schedule:not(.has-photo):not(.no-overflow-menu):has(.task-text[contenteditable="true"]) .task-schedule-pill {
      --sched-shift: -32px;
    }
    /* Stays compact even in touch mode — .task-due/.task-recurrence-pill's own touch
       override (below, "bigger tap target for the inline tags-row use") would otherwise
       make this corner badge nearly as tall as the row itself; this isn't a tap-target-
       sizing context, it's a small always-visible label, so it keeps the same compact
       size on every pointer type. */
    .touch-mode .task-due.task-schedule-pill,
    .touch-mode .task-recurrence-pill.task-schedule-pill {
      min-height: 0; padding: 2px 8px; font-size: .7rem;
    }
    /* Reserves room in the title's own column, same idea as .has-photo .task-text-wrap
       just above (unscoped value = touch, narrowed for desktop) — sized for the widest
       realistic pill text ("weekly · Wed"), combined with the photo's own reservation
       when both are present. */
    .has-schedule .task-text-wrap { margin-right: 112px; }
    .has-photo.has-schedule .task-text-wrap { margin-right: 170px; }
    body:not(.touch-mode) .has-schedule .task-text-wrap { margin-right: 76px; }
    body:not(.touch-mode) .has-photo.has-schedule .task-text-wrap { margin-right: 135px; }

    /* Photo editor: crop stage */
    .photo-modal { width: min(420px, calc(100vw - 32px)); }
    .photo-editor { display: flex; flex-direction: column; gap: 10px; }
    .photo-crop-stage { position: relative; width: 100%; line-height: 0; }
    /* pointer-events: none (2026-08-29) — a long-press on a plain <img> triggers the
       browser/OS's own "save/copy image" context menu (Android's "Download or copy
       photo" sheet), which fought with dragging the crop handles right on top of it.
       All the actual crop interaction already happens on .photo-crop-overlay (the SVG
       sitting directly on top, same size/position) via its own pointer listeners, not on
       the image itself, so the image never needed to receive touches in the first
       place — with pointer-events off, a long-press passes straight through to the SVG
       instead of hitting an <img> at all, and the native menu never has a reason to
       appear. The extra properties below are the standard belt-and-suspenders for the
       same goal on browsers that still try (older Android WebViews, iOS Safari). */
    .photo-crop-img {
      display: block; width: 100%; border-radius: 4px;
      pointer-events: none; -webkit-touch-callout: none;
      -webkit-user-select: none; user-select: none;
    }
    /* overflow: visible (2026-08-28) — an SVG root clips to its own box by default, and
       the default crop is the whole photo (handles sitting exactly at each corner), so
       without this every handle had 3/4 of its circle clipped away by the box edge,
       leaving only the inward-facing quarter actually visible/clickable. */
    .photo-crop-overlay { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; overflow: visible; }
    .photo-crop-handle { cursor: pointer; }
    .task-note-area textarea {
      display: block; width: 100%; margin: 0; border: none; background: transparent;
      padding: 0; font-size: .84rem; font-family: inherit;
      resize: none; min-height: 0; overflow: hidden; line-height: 1.6;
      color: var(--text); outline: none;
    }
    .note-view {
      font-size: .84rem; line-height: 1.6; color: var(--muted);
      padding: 0; cursor: text; font-weight: 400;
      word-break: break-word;
    }
    /* Real gap found live (2026-09-12 follow-up) — Malte: "on the 'Add a note...' field in
       the full screen task dialog. Remove the outline - also the padding changes there
       when selecting that row." The full-screen dialog builds its own notes textarea
       (buildTaskDetailBody, unlike the list row's own note editor, which already goes
       through `.task-note-area textarea`'s reset above) with nothing resetting the
       browser's own UA-default `<textarea>` border/background — confirmed live,
       `0.571429px solid rgb(118,118,118)`, invisible on top of a plain page background but
       readable as "an outline" here, and its own box-sizing:border-box eats into the
       content area by that same border width, reading as the padding shift Malte saw the
       moment the empty-state placeholder (a plain `<div>`, no border) turns into this
       `<textarea>`. `font-family`/`margin` reset for the same reason `.task-note-area
       textarea` resets them — a bare `<textarea>` doesn't inherit either from its
       surroundings by default. Scoped to the tag, not folded into `.note-view` itself,
       since the plain `<div>` variant (the has-content/empty-state display, not editing)
       has never had a border/background to reset in the first place. */
    /* display:block found missing on a closer look (2026-09-12 follow-up) — Malte kept
       pushing on "still one minimal change in row height" after font-size/line-height
       were both confirmed byte-identical, and the actual remaining ~4px turned out to be
       a completely different, well-known CSS gap: a bare `<textarea>` defaults to
       `display: inline-block`, which — sitting inside `.task-detail-notes` as inline-level
       content — reserves room for a full inline line box (baseline/descender space)
       around it, taller than the element's own content box. The empty-state placeholder
       it replaces is a plain `<div>` (`display: block` by default), which has no such
       gap, so the wrapping `.task-detail-notes` div measured 25.5px around this textarea
       against 21.5px around the placeholder — identical textarea/div content heights,
       confirmed live, the wrapper was the one differing. `.task-note-area textarea` (the
       list row's own separate note editor) already has this exact same `display: block`
       in its own reset, for the identical reason — this rule just never got it. */
    textarea.note-view { display: block; border: none; background: transparent; margin: 0; font-family: inherit; }
    /* No visible focus outline while editing (2026-09-12) — see .task-text:focus's own
       comment; same ask, same fix, this is the notes textarea's own equivalent ring. */
    textarea.note-view:focus { outline: none; }
    /* Second bug stacked on the same field, found on Malte's own "check again — still an
       outline or border, and the field is bigger when input is active": this dialog's
       modal wrapper is itself a `.sec-edit-modal` (every full-screen/floating dialog in
       this app shares that one class), and `.sec-edit-modal`'s own generic
       input/select/textarea reset — several hundred lines down this same file — matches
       this exact element too, with the *same* specificity as the fix just above (one
       class + one tag either way). A tie like that is decided by source order, not
       intent, and `.sec-edit-modal textarea`'s own border/padding/font-size/background
       sits later in the file — so it silently won every one of those properties inside
       this dialog specifically, regardless of what the fix above said, while looking
       completely fixed anywhere tested outside a `.sec-edit-modal` wrapper (exactly how
       the first pass's own live test missed it). This qualifier outranks it on genuine
       specificity (two classes + one tag) instead of relying on file position.
       font-size originally left out of this override, on purpose — in touch mode,
       `.touch-mode .note-view`'s own 16px (iOS anti-zoom-on-focus) already correctly beat
       `.sec-edit-modal textarea`'s .9rem with no help needed, and adding this class into
       that fight risked flipping it by accident. Malte then caught the actual visible
       consequence on desktop: "the text size seems to be different (smaller when not
       selected)" — desktop has no such competing rule, so leaving font-size out just meant
       `.sec-edit-modal textarea`'s .9rem won *there* uncontested, sitting bigger than the
       rendered note's own .84rem the moment you click to edit. Added back below, scoped to
       exclude touch mode specifically so it can't re-risk that original fight. */
    .sec-edit-modal textarea.note-view { padding: 0; border: none; border-radius: 0; background: transparent; }
    .sec-edit-modal textarea.note-view:focus { border-color: transparent; outline: none; }
    /* Desktop-only font-size match (2026-09-12 follow-up) — see the comment above for why
       this couldn't just join that rule. `:not(.touch-mode)` makes this and
       `.touch-mode .note-view`'s own 16px mutually exclusive by construction (never both
       match the same element), so there's no specificity fight to accidentally lose here
       even though this selector's own specificity is higher. */
    body:not(.touch-mode) .sec-edit-modal textarea.note-view { font-size: .84rem; }
    .note-view p, .note-editor p { margin: 0; }
    .note-view h1, .note-editor h1 { font-size: 1.05rem; font-weight: 700; color: var(--text); margin: .5em 0 .2em; }
    .note-view h2, .note-editor h2 { font-size: .95rem; font-weight: 700; color: var(--text); margin: .5em 0 .2em; }
    .note-view h3, .note-editor h3 { font-size: .88rem; font-weight: 700; color: var(--text); margin: .4em 0 .15em; }
    .note-view ul, .note-editor ul, .note-view ol, .note-editor ol { margin: .2em 0 .4em 1.2em; padding: 0; }
    .note-view li, .note-editor li { margin: .1em 0; }
    .note-view blockquote, .note-editor blockquote { border-left: 3px solid var(--border); margin: .3em 0 .3em .2em; padding-left: .7em; color: var(--faint); }
    .note-view code, .note-editor code { font-family: monospace; font-size: .82rem; background: var(--bg); border: 1px solid var(--border); border-radius: 3px; padding: 0 3px; }
    .note-view pre, .note-editor pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r); padding: 8px 10px; overflow-x: auto; margin: .3em 0; }
    .note-view pre code, .note-editor pre code { background: none; border: none; padding: 0; }
    .note-view hr, .note-editor hr { border: none; border-top: 1px solid var(--border); margin: .5em 0; }
    .note-view strong, .note-editor strong { font-weight: 700; color: var(--text); }
    .note-view em, .note-editor em { font-style: italic; }
    .note-view del, .note-editor del { text-decoration: line-through; opacity: .6; }
    .note-view a, .note-editor a, .task-text a { color: var(--accent); text-decoration: underline; }
    .note-view a:hover, .note-editor a:hover, .task-text a:hover { color: var(--accent2); }
    .note-view .md-checkbox { display: flex; align-items: baseline; gap: 5px; margin: .1em 0; }
    .note-view .md-checkbox input { cursor: pointer; margin: 0; flex-shrink: 0; position: relative; top: 1px; }
    .note-view .md-checkbox.checked span { text-decoration: line-through; opacity: .5; }
    .task.expanded { background: #fff; }
    .task.overflow-active,
    .task:has(.task-text[contenteditable="true"]) { background: #fff; }
    /* cursor:pointer overrides .note-view's own base cursor:text (2026-09-11 follow-up)
       — this placeholder (and, since the same day, its "Add a schedule…"/"Add media…"
       siblings reusing this exact class for a consistent look) is never actually
       editable in place, it always switches to something else on click — a text-input
       cursor was never quite right for it, more so now that the whole containing row is
       the click target too, not just this text. */
    .note-view.note-empty { color: var(--faint); font-style: italic; cursor: pointer; }



    /* section footer — touch-only "+ task" (2026-09-02, backlog #7); Done stays reachable
       only via the section's own "⋯" menu (openDoneTasksDialog). .repeating-toggle was
       removed the same day Done/Repeating moved into that menu, then brought back
       2026-09-10 (Malte: "bring back the middot repeating... that would toggle the
       visibility of the task down below") — now scoped to just the done-for-cycle
       repeating block (renderSection's own doneRepeatingTasks) instead of every
       repeating task, since it's the one that actually needs a show/hide affordance
       now that it's a dedicated trailing block rather than part of `sec.tasks` itself. */
    .section-footer { display: flex; align-items: center; gap: 4px; padding: 6px 16px 0; }
    .ft-add-btn .ft-count, .repeating-toggle .ft-count { font-family: 'Figtree', sans-serif; font-size: 10px; font-weight: 400; color: var(--text); }
    .ft-add-btn .ft-label, .repeating-toggle .ft-label {
      font-family: 'Figtree', sans-serif;
      font-size: 10px; font-weight: 400;
      letter-spacing: 1.8px; line-height: 15px;
      text-transform: uppercase;
    }
    .ft-add-btn, .repeating-toggle {
      display: inline-flex; align-items: center; gap: 4px;
      background: none; border: none; padding: 0; cursor: pointer;
      color: var(--faint); transition: color .12s;
    }
    .ft-add-btn:hover, .repeating-toggle:hover { color: var(--muted); }
    .repeating-toggle.open { color: #000; }
    .touch-mode .ft-add-btn .ft-label, .touch-mode .repeating-toggle .ft-label { font-size: 13px; }
    .touch-mode .section-footer { padding: 10px 16px 2px; gap: 6px; }
    /* .repeating-list (the Repeating-tasks dialog's own wrapper) removed 2026-09-10
       along with that whole dialog and its section-menu entry — Malte: "remove the
       repeating entry from the section overflow menu and the whole dialog that came
       with it." Nothing else used those rules. */
    .task:has(.task-recurrence-info) .task-row { padding-bottom: 2px; }
    .task:has(.task-check) .task-recurrence-info { padding-left: 44px; }
    @keyframes rep-dot-blink {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.12; }
    }
    .task-repeat-dot {
      width: 7px; height: 7px; border-radius: 50%;
      background: #F09C30; display: inline-block;
      vertical-align: middle; margin-left: 5px; flex-shrink: 0;
      animation: rep-dot-blink 2.4s ease-in-out infinite;
    }
    .task-recurrence-pill {
      font-size: .7rem; padding: 2px 8px; border-radius: 10px; cursor: pointer;
      display: inline-flex; align-items: center; gap: 3px; user-select: none; flex-shrink: 0;
    }
    .task-recurrence-pill:hover { filter: brightness(0.93); }
    .task-recurrence-info {
      font-size: .72rem; color: var(--muted);
      padding: 0 16px 5px 16px;
      display: flex; gap: 6px; align-items: center;
    }
    .task-recurrence-info:last-of-type { padding-bottom: 16px; }
    .task-recurrence-info .rep-active { color: #F09C30; font-weight: 400; }
    .task-text .rep-active { font-size: .72rem; font-weight: 400; color: #F09C30; vertical-align: middle; }
    .task-repeat-icon { display: inline-flex; align-items: center; color: var(--accent); opacity: .55; margin-left: 4px; flex-shrink: 0; }
    .task-repeat-icon svg { width: 11px; height: 11px; }

    .done-list { display: flex; flex-direction: column; gap: 4px; margin: 6px; }
    .bad-habit-chk {
      width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0; margin-top: 1px;
      border: 2px solid transparent; background: transparent; cursor: pointer;
      position: relative; display: inline-flex; align-items: center; justify-content: center;
      transition: border-color .2s, background .2s, box-shadow .2s;
    }
    .bad-habit-chk .bhc-icon { font-size: 22px; color: var(--text); opacity: .35; pointer-events: none; transition: opacity .2s; }
    .bad-habit-chk:hover .bhc-icon { opacity: .6; }
    .bad-habit-chk.unlocked { border-color: var(--danger); background: rgba(220,53,69,.07); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
    .bad-habit-chk.unlocked:hover { border-color: var(--danger); }
    .task:has(.bad-habit-chk) .task-recurrence-info { padding-left: 44px; }
    .done-task {
      display: flex; align-items: flex-start; gap: 10px;
      padding: 9px 10px 9px 10px;
      background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r); opacity: .55;
      box-shadow: 0 1px 2px rgba(0,0,0,.04);
      transition: opacity .15s;
    }
    .done-task:hover { opacity: .75; }
    .done-task:hover .task-check { border-color: #7a9ab8; }
    .done-task .task-check:checked::after { left: 5px; top: -1px; }
    .done-task-body { flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; }
    .done-task-txt { font-size: .9rem; text-decoration: line-through; color: var(--muted); line-height: 1.45; word-break: break-all; overflow-wrap: anywhere; }
    .done-task-note { font-size: .78rem; color: var(--faint); font-style: italic; line-height: 1.4; word-break: break-all; overflow-wrap: anywhere; }
    .done-task .task-tags { padding: 0; }
    .done-task .del {
      background: none; border: none; cursor: pointer;
      color: transparent; font-size: .82rem; padding: 2px 4px;
      border-radius: 4px; flex-shrink: 0; transition: color .12s;
    }
    .done-task:hover .del { color: #b0bfce; }
    .done-task .del:hover { color: var(--danger) !important; }
    .touch-mode .done-task .del { color: #b0bfce; } /* no hover on touch — keep it permanently visible */


    /* Gem Jar (2026-09-02, single-row redesign 2026-09-03) — one row, no separate name
       or always-visible block below: the jar icon sits where a checkbox normally would
       (a fixed anchor, not a conditional empty-state hint), the gems themselves appear
       right next to it and wrap within the row as they accumulate, then the count badge
       and +/- buttons follow. Everything centered against the buttons (2026-09-03
       follow-up), unlike an ordinary task row's own top-aligned checkbox. */
    .gemjar-row { align-items: center; }
    /* position: relative so .gemjar-count (2026-09-03 follow-up) can be layered directly
       on top of the icon, absolutely centered a little below its middle — the jar's body
       rather than its lid — so the number reads as sitting inside the jar itself instead
       of as a separate badge next to it. */
    /* width: 1em, not a fixed px value (2026-09-12 follow-up) — Malte: "the Henri is not
       vertically aligned with the others. It seems to sit 1-2px more to the right... on
       mobile and desktop." Root cause: this box's width was tuned in isolation on
       2026-09-03 to match the *checkbox's* own 20px/24px column (the "sits where a
       checkbox normally would" framing in the comment above), while `i.task-app-icon`
       (every other section app's own icon, added later) has no fixed width at all — it's
       sized purely by its own glyph's intrinsic footprint, which Phosphor's icon font
       renders as a square box that's for practical purposes exactly 1em on a side
       (confirmed live: 23.05px measured at font-size 1.44rem = 23.04px). Two independently
       reasonable widths, never actually compared against each other until now. `width: 1em`
       ties this box to the same "one font-size, one square em box" rule i.task-app-icon
       already gets for free, so the two stay aligned automatically — including through any
       future font-size retuning of either one — instead of two hand-picked px values that
       happened to land close but not equal. */
    .gemjar-jar {
      flex-shrink: 0; width: 1em; font-size: 1.2rem;
      line-height: 1; text-align: center; position: relative; cursor: pointer;
    }
    .gemjar-gems {
      flex: 1; min-width: 60px; display: flex; flex-wrap: wrap; align-items: center;
      gap: 2px; font-size: 1.15rem; line-height: 1.3; word-break: break-all; overflow-wrap: anywhere;
    }
    .gemjar-count {
      position: absolute; left: 50%; top: 60%; transform: translate(-50%, -50%);
      font-size: .5rem; font-weight: 800; line-height: 1; color: var(--text);
      white-space: nowrap; pointer-events: none;
    }
    /* Bigger, real touch-target sized (2026-09-02 — were 26px, easy to fat-finger). Kept
       full-size in touch mode (2026-09-11 follow-up doesn't shrink this there at all —
       see the negative margin just below for how the row stays compact anyway without
       needing to), preserving that earlier usability fix rather than trading it away for
       row-height consistency. Desktop is a separate story — see body:not(.touch-mode)
       just below, added the same day at Malte's own follow-up ask ("decrease the size of
       the gem jar buttons" on desktop specifically): a mouse doesn't need a fat-finger-
       proof target the way touch does, so there's no real reason for these to stay
       44px there once that's the only reason they were that big to begin with. */
    .gemjar-btn {
      flex-shrink: 0; font-size: 1.05rem; font-weight: 600; font-family: inherit;
      color: var(--text); background: var(--bg); border: 1px solid var(--border);
      border-radius: 999px; height: 44px; padding: 0 18px; line-height: 1;
      cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px;
      /* Negative vertical margin, not a smaller row (2026-09-11 follow-up) — Malte: "make
         this in it's initial state... the same height as everything else." At rest (0-3
         gems, one line) this row was 66px tall (44px button + the row's own 22px
         padding) against ordinary/other section-app rows' ~42-44px — the button, not the
         jar icon or a line of gems, was what forced the extra height. A *smaller* button
         would fix that, but would also undo the exact "easy to fat-finger" usability fix
         from 2026-09-02 this same button already got, especially in touch mode.
         Negative margin instead removes the button's height from the row's own natural
         (auto) sizing without shrinking the button itself: the row's height reverts to
         whatever its *other* content (the jar icon, and the gems themselves) would need
         on their own, matching ordinary rows again once that's the tallest thing in it,
         while the button still renders and taps at its full 44px — confirmed live
         (getBoundingClientRect) it neither overflows past the row's own edges nor
         reintroduces the pre-2026-09-02 fat-finger problem, in both pointer contexts,
         with the exact same value. Multi-line gems still genuinely grow the row past
         this floor when there's more than one line of them (Malte: "at least before
         there is more than one line of diamonds") — this only ever removes the
         *button's* contribution to the row's own auto-height, never the gems'. */
      margin-top: -11px; margin-bottom: -11px;
    }
    /* Desktop-only shrink (2026-09-11 follow-up) — Malte: "decrease the size of the gem
       jar buttons" on desktop. Once the button's own height is small enough to fit the
       row's natural content height on its own, the negative-margin trick above becomes
       unnecessary here (reset to 0) rather than compounding with a now-smaller button —
       confirmed live (getBoundingClientRect): 22px lands the row within half a pixel of
       an ordinary task's own desktop height, the same close-enough tolerance every other
       row-height match this session has used, with no overflow trick needed at all. */
    body:not(.touch-mode) .gemjar-btn {
      height: 22px; padding: 0 12px; font-size: .8rem;
      margin-top: 0; margin-bottom: 0;
    }
    .gemjar-btn:hover { background: var(--border); }
    .gemjar-btn:disabled { opacity: .35; cursor: default; }
    /* Touch tap leaves the button in :focus until something else is tapped, and the UA's
       default focus outline (2026-09-12) — Malte: "the add diamond button, for touch this
       stays focussed and looks off. focus state should be the same as inactive." Scoped
       to touch mode only, not a blanket outline:none, so keyboard/desktop users still get
       a real focus ring when tabbing to these. */
    .touch-mode .gemjar-btn:focus { outline: none; }
    .gemjar-btn-plus { background: var(--accent); color: #fff; border-color: var(--accent); }
    .gemjar-btn-plus:hover { filter: brightness(1.08); }
    /* The icon on each button takes its color from the button itself (currentColor) —
       dark on the plain minus, white on the filled plus — rather than the gems' own
       fixed vibrant blue (2026-09-03). */
    .gemjar-btn .ph-sketch-logo { font-size: 1rem; }
    .gemjar-overflow { font-size: .8rem; color: var(--faint); font-weight: 600; }
    /* Empty-state name (2026-09-12) — see the .gemjar-empty-name comment in
       renderGemJarTask for why this exists; sized/colored like the other section apps'
       own always-visible name labels (.metric-task-name etc.) rather than the gems'
       larger 1.15rem, since it's standing in for that same "what is this" label, just
       conditionally. display toggled in JS (refreshChrome), not a CSS :empty rule —
       .gemjar-gems needs to stay flex regardless of which children it has.
       line-height is a fixed 1.495rem, NOT the usual unitless multiplier every other
       name class uses (2026-09-12 follow-up — Malte: "adding the first gem make the row
       a few pixels less high - so a small jump in height happens between empty state and
       first diamond"). A gem's own actual line box is 1.3 (.gemjar-gems' own unitless
       line-height) × 1.15rem (its own font-size) = 1.495rem, fixed regardless of touch
       mode since .gemjar-gems itself has no touch-mode override. This name's font-size
       does change between desktop/touch (see the touch-mode rule just below, matching
       every other title there) — with a unitless line-height that would've meant ITS line
       box scaling with that font-size change too, landing at a different height than a
       gem's own in at least one of the two modes (confirmed live: 21.84px this name vs
       23.92px a gem, in touch mode, after just fixing the font-size match). Hardcoding
       the same absolute value a gem gets keeps this row's height identical in the empty
       and non-empty states, in both pointer contexts, rather than solving it in only one. */
    .gemjar-empty-name { font-size: .9375rem; line-height: 1.495rem; color: var(--text); }
    /* Font-size only (2026-09-12 follow-up) — matches the shared touch-mode bump every
       other section-app name class gets (see that rule further down), kept as its own
       standalone declaration instead of joining that shared selector because this one
       specific line-height stays fixed instead of following along — see the comment on
       the base rule just above for why. */
    .touch-mode .gemjar-empty-name { font-size: 1.05rem; }
    /* Reappearance fade (2026-09-12 follow-up) — Malte: "when removing the last diamond
       the reappearance of the name should wait until the last diamond clears before it
       fades in again." Applied via JS only on that specific path (minusBtn's own
       animationend handler, once the gem itself has actually finished vanishing) — never
       on the initial render or on going non-empty->empty any other way, where an instant
       appearance is still correct/expected. */
    @keyframes gemjar-name-fade-in { from { opacity: 0; } to { opacity: 1; } }
    .gemjar-name-fade { animation: gemjar-name-fade-in .25s ease; }
    /* Sketch's diamond-shaped logo glyph standing in for a gem. Each gem gets its own
       random pick off the app's main vibrant palette (2026-09-03 follow-up, --gc set
       inline per gem in mkGemSpan, falling back to the jar's own blue if somehow unset)
       instead of one fixed color, plus a small random rotation + scale (--gr/--gs) so a
       jar full of them reads as organically tossed in and genuinely varied rather than a
       perfectly straight row of identical icons. */
    .gemjar-gem { display: inline-block; color: var(--gc, var(--accent)); transform: rotate(var(--gr, 0deg)) scale(var(--gs, 1)); }
    /* Individual gem animations — a new gem pops in with a little overshoot-bounce
       (matches the app's existing playful confetti tone); a removed one plays the exact
       same animation in reverse (2026-09-03 — was its own separate shrink/spin/float-up
       keyframes), so pulling gems out feels as immediate as adding them rather than a
       slower, different-feeling exit. `animation-fill-mode: forwards` is what makes the
       reverse direction end holding the 0% frame's shrunk/invisible state instead of
       snapping back to rest. Real @keyframes, not a transition — a freshly-inserted
       element has no "previous value" a transition could animate from, and
       celebrateCheck's own confetti anchors on this same element right after it's
       inserted, so the pop-in needs to already be running by then, not waiting on a
       forced reflow. The 100% frame reads the gem's own --gr/--gs so both directions
       meet exactly at its resting rotate/scale, with no visible snap. Note: removal no
       longer waits on this animation to finish before committing the count change — see
       the minus button's own onclick for why that used to make removing gems feel slow
       (and could even double-decrement on a quick second click). */
    .gemjar-gem-new { animation: gemjarPopIn .45s cubic-bezier(.34,1.56,.64,1); }
    .gemjar-gem-vanish { animation: gemjarPopIn .32s cubic-bezier(.34,1.56,.64,1) reverse forwards; }
    @keyframes gemjarPopIn {
      0%   { transform: rotate(-20deg) scale(0); opacity: 0; }
      55%  { transform: rotate(calc(var(--gr, 0deg) + 14deg)) scale(calc(var(--gs, 1) * 1.3)); opacity: 1; }
      100% { transform: rotate(var(--gr, 0deg)) scale(var(--gs, 1)); opacity: 1; }
    }

    /* Gem Jar's redemption-list dialog (2026-09-03) — a list of directly editable
       reward rows. */
    .gemjar-rewards-list { display: flex; flex-direction: column; gap: 4px; }
    .gemjar-reward-row {
      display: flex; align-items: center; gap: 6px;
      padding: 6px 8px; background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r);
    }
    /* Dimmed, not hidden or disabled (2026-09-03) — still a real reward, just not
       affordable with today's count; a quick glance at what's already in reach vs. what
       still needs saving up for. */
    .gemjar-reward-row.gemjar-reward-unaffordable { opacity: .45; }
    .gemjar-reward-row input[type=text].gemjar-reward-emoji {
      width: 2.2em; flex-shrink: 0; text-align: center; padding: 6px 2px; font-size: 1.05rem;
    }
    .gemjar-reward-row input[type=text].gemjar-reward-label { flex: 1; min-width: 0; }
    .gemjar-reward-cost { flex-shrink: 0; display: flex; align-items: center; gap: 3px; color: var(--accent); }
    .gemjar-reward-cost .ph-sketch-logo { font-size: .95rem; }
    .gemjar-reward-row input[type=number].gemjar-reward-cost-input {
      width: 2.6em; flex-shrink: 0; text-align: center; padding: 6px 2px;
    }
    .gemjar-reward-delete {
      background: none; border: none; cursor: pointer; color: #b0bfce;
      font-size: .82rem; padding: 4px 6px; border-radius: 4px; flex-shrink: 0; transition: color .12s;
    }
    .gemjar-reward-delete:hover { color: var(--danger); }

    /* Shopping-list mode (2026-09-12) — see 19_section_shopping_mode.sql's own comment for
       the full design discussion behind this whole feature. */

    /* Autocomplete dropdown under the new-item draft input (renderGap), shopping-mode
       sections only. position:relative is harmless for every other (non-dropdown) use of
       .task-text-wrap elsewhere in the app — no offset properties set, so it changes
       nothing unless an absolutely-positioned child (this dropdown) actually exists. */
    .task-text-wrap { position: relative; }
    .shopping-suggest {
      position: absolute; left: 0; right: 0; top: 100%; margin-top: 4px; z-index: 20;
      background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
      box-shadow: 0 4px 16px rgba(0,0,0,.14); overflow: hidden; display: none;
      max-height: 260px; overflow-y: auto;
    }
    /* Opens upward on touch (2026-09-13 follow-up) — Malte: "On mobile I cannot see it
       properly, because the keyboard hides it. Should appear to the top of the task."
       The on-screen keyboard eats the bottom third-or-so of the viewport, and the draft's
       own input sits right above it — a dropdown opening *downward* from there lands
       mostly or entirely underneath the keyboard. Flipped to bottom:100% here instead,
       desktop keeps opening downward (no keyboard to dodge, and it reads more naturally
       there next to the arrow-key navigation just below). */
    .touch-mode .shopping-suggest { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 4px; }
    /* Each suggestion is now a row (item text + its own "x"), not one big button — a
       button can't nest another interactive element, and the remove button (2026-09-13,
       see .shopping-suggest-remove below) needs its own separate hit target regardless. */
    .shopping-suggest-item { display: flex; align-items: stretch; }
    .shopping-suggest-item:hover, .shopping-suggest-item.active {
      background: var(--bg);
    }
    .shopping-suggest-pick {
      flex: 1; min-width: 0; display: block; text-align: left; padding: 8px 12px; font-size: .9rem;
      font-family: inherit; color: var(--text); background: none; border: none; cursor: pointer;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .touch-mode .shopping-suggest-pick { padding: 11px 14px; font-size: 1rem; }
    /* The "x" (2026-09-13) — Malte: "Maybe add an 'x' at the end of each suggestion, to
       remove wrong ones from the pool." Muted until hovered/tapped, same restraint as
       every other small icon-only affordance in the app (.sec-more-btn). */
    .shopping-suggest-remove {
      flex-shrink: 0; display: flex; align-items: center; justify-content: center;
      padding: 0 10px; border: none; background: none; color: var(--faint); cursor: pointer;
    }
    .shopping-suggest-remove:hover { color: var(--text); }
    .shopping-suggest-remove i { font-size: .85rem; }
    .touch-mode .shopping-suggest-remove { padding: 0 14px; }
    .touch-mode .shopping-suggest-remove i { font-size: 1rem; }

    /* "No photo yet" affordance on an ordinary shopping-mode task's own row (2026-09-12,
       revised 2026-09-13) — same slot .task-photo-thumb-btn's real thumbnail sits in once
       a photo exists (this button shares that class for the positioning/sizing, plus this
       one for the icon styling instead of an <img>). Hidden by default, same
       opacity/pointer-events reveal shape as .task-row-overflow's own (just below it in
       this file) — Malte: "Only show it when the task title is in editing mode." Only one
       of that button's own three triggers actually applies here though: this task has no
       overflow menu to reveal in the first place (.expanded doesn't happen for a
       shopping-mode task at all, see app.js's own getShowNotes() guard), so the title
       being mid-edit is the only real signal left. Position itself never shifts either —
       see --photo-shift's own :not(.no-overflow-menu) exclusion above, which this button
       inherits for free via the shared .task-photo-thumb-btn class. */
    .task-photo-add-btn {
      display: flex; align-items: center; justify-content: center; font-size: 1.1rem; color: var(--muted);
      opacity: 0; pointer-events: none; transition: opacity .18s ease;
    }
    .task:has(.task-text[contenteditable="true"]) .task-photo-add-btn { opacity: 1; pointer-events: auto; }
    .task-photo-add-btn:hover { color: var(--accent); }

    /* "Manage store cards" dialog (openStoreCardsDialog, 2026-09-12) — same "list of
       editable rows" shape the gem jar's own rewards dialog already established just
       above (.gemjar-reward-row etc.), iterating tasks instead of one task's own array. */
    .storecard-manage-list { display: flex; flex-direction: column; gap: 4px; }
    .storecard-manage-row {
      display: flex; align-items: center; gap: 8px;
      padding: 6px 8px; background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r);
    }
    .storecard-manage-thumb {
      flex-shrink: 0; width: 40px; height: 40px; border-radius: 6px;
      border: 1px solid var(--border); background: var(--bg); padding: 0; cursor: pointer;
      display: flex; align-items: center; justify-content: center; overflow: hidden;
      font-size: 1.1rem; color: var(--muted);
    }
    .storecard-manage-thumb:hover { color: var(--accent); }
    .storecard-manage-thumb img { width: 100%; height: 100%; object-fit: cover; }
    /* Specificity beats .sec-edit-modal input[type=text]'s own generic reset (width:100%
       among others) on genuine specificity — two classes plus the type attribute here vs.
       one class there — not source order; see today's earlier textarea.note-view fix for
       the identical lesson learned the hard way. */
    .storecard-manage-row input[type=text].storecard-manage-name { flex: 1; min-width: 0; width: auto; }
    .storecard-manage-delete {
      background: none; border: none; cursor: pointer; color: #b0bfce;
      font-size: .82rem; padding: 4px 6px; border-radius: 4px; flex-shrink: 0; transition: color .12s;
    }
    .storecard-manage-delete:hover { color: var(--danger); }

    /* "Grocery lists" dialog (openGroceryListsDialog, 2026-09-13; merged into one dialog
       and redesigned to flow like the ordinary task list, 2026-09-13 follow-ups) — Malte:
       "make the add from list and manage list into one dialog... the benefit would be
       that I can directly see what will be added anyway." / "Make the add to list button
       more prominent, a wide button... Overall make the design of that page a bit more
       like that current ordinary task design. First row: Title. Second row: Add to list
       button. Third++ rows: [items]." No individual bordered card per set any more —
       each is a plain vertical block (title, wide add-button, items), separated from the
       next by .grocery-set-block's own border-bottom, the same "rows flowing one after
       another, not boxes stacked in a pile" feel the main task list already has. */
    .grocery-sets-list { display: flex; flex-direction: column; }
    .grocery-set-block { padding: 14px 0; border-bottom: 1px solid var(--border); }
    .grocery-set-block:first-child { padding-top: 2px; }
    .grocery-set-block:last-child { border-bottom: none; padding-bottom: 2px; }

    /* Row 1: title — borderless and bold, reading as a real heading rather than a form
       field until it's actually touched, the same restraint .task-text itself uses for
       inline-editable text. */
    .grocery-set-title-row { display: flex; align-items: center; gap: 4px; }
    .grocery-set-title-row input[type=text].grocery-set-name {
      flex: 1; min-width: 0; width: auto; font-weight: 700; font-size: 1rem;
      border: none; background: none; padding: 4px 2px; color: var(--text);
    }
    .grocery-set-title-row input[type=text].grocery-set-name:focus { background: var(--bg); border-radius: 4px; }
    .grocery-set-delete {
      background: none; border: none; cursor: pointer; color: #b0bfce;
      font-size: .82rem; padding: 4px 6px; border-radius: 4px; flex-shrink: 0; transition: color .12s;
    }
    .grocery-set-delete:hover { color: var(--danger); }

    /* Row 2: "Add to list" — a full-width, unmissable bar directly under the title, not
       a small icon (2026-09-13 follow-up: "Make the add to list button more prominent, a
       wide button"). Its own label carries the live item count (see renderItems' own
       innerHTML update), so the button doubles as a preview of what tapping it will do. */
    .grocery-set-addbar {
      display: flex; align-items: center; justify-content: center; gap: 8px;
      width: 100%; margin: 8px 0; padding: 10px 12px;
      background: var(--accent-bg); color: var(--accent2); border: 1px solid transparent;
      border-radius: var(--r); font-size: .9rem; font-weight: 600; font-family: inherit;
      cursor: pointer; transition: background .12s;
    }
    .grocery-set-addbar:hover { background: #cdeafc; }
    .grocery-set-addbar:disabled { background: var(--bg); color: var(--faint); cursor: default; }
    .grocery-set-addbar i { font-size: 1rem; }

    /* Rows 3+: items — plain, no per-row box either, matching the same "looks like the
       real list" intent as the title above. */
    .grocery-set-items { display: flex; flex-direction: column; }
    .grocery-item-row {
      display: flex; align-items: center; gap: 6px;
      padding: 7px 2px; border-radius: 6px;
    }
    .grocery-item-row input[type=text].grocery-item-input {
      flex: 1; min-width: 0; width: auto; padding: 5px 6px; font-size: .9rem;
    }
    .grocery-item-delete {
      background: none; border: none; cursor: pointer; color: #b0bfce;
      font-size: .78rem; padding: 4px 6px; border-radius: 4px; flex-shrink: 0; transition: color .12s;
    }
    .grocery-item-delete:hover { color: var(--danger); }
    /* The trailing "Add item…" row has nothing to delete yet — no icon column to align
       against, so its own input gets a touch more left padding instead of sitting flush
       with the real rows' text above it. */
    .grocery-item-add-row input[type=text].grocery-item-input { padding-left: 10px; }

    /* "Default for the quick-add row" — a small caption-weight variant of the shared
       .repeat-picker-row checkbox toggle (openSectionEditor's own "Shopping list" toggle
       established the base look), muted and compact rather than as prominent as the
       list's own title above it. */
    .grocery-set-default-row { margin: 2px 0 10px; font-size: .78rem; font-weight: 400; color: var(--muted); }

    /* Pinned grocery quick-add row (renderGroceryQuickAddRow, 2026-09-13) — Malte: "an
       entry at the top of the shopping list... same height as ordinary task, same
       dragable handle... a button visible on the right - similar style to that camera
       icon... opens the grocery list with all the lists." .grocery-quick-add-row reuses
       .task's own background/positioning for free (same class); this row alone needs
       align-items:center instead of .task-row's own flex-start default, since (unlike an
       ordinary task) there's no multi-line note content here ever needing top-alignment. */
    .grocery-quick-add-taskrow { align-items: center; }
    /* Heights matched to an ordinary row's own content height exactly (22.5px desktop,
       26px touch — that row's own padding, 22px/36px, plus this equals 44.5px/62px,
       measured directly against a real task row) — not .task-photo-thumb-btn's own
       24px/40px, which was sized for a *secondary* action sitting beside real title
       text, not this row's one and only content. */
    .grocery-quick-add-btn {
      flex: 1; min-width: 0; height: 22.5px; display: flex; align-items: center; gap: 8px;
      padding: 0 10px; background: var(--accent-bg); color: var(--accent2);
      border: none; border-radius: var(--r); font-size: .85rem; font-weight: 600;
      font-family: inherit; cursor: pointer; overflow: hidden; transition: background .12s;
    }
    .grocery-quick-add-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .grocery-quick-add-btn i { font-size: 1rem; flex-shrink: 0; }
    .grocery-quick-add-btn:hover { background: #cdeafc; }
    .touch-mode .grocery-quick-add-btn { height: 26px; font-size: .95rem; }
    /* Room for the absolutely-positioned open-button beyond this row's right edge — same
       42px/64px reservation .has-photo .task-text-wrap already uses for the exact same
       reason (a real photo thumbnail sitting in that same spot). Only the "has a default
       list" variant of this button carries this class — the empty-state prompt's own
       trailing "x" sits in normal flex flow right after it instead, needing no such gap. */
    .grocery-quick-add-reserve { margin-right: 42px; }
    .touch-mode .grocery-quick-add-reserve { margin-right: 64px; }
    /* The right-hand "open all lists" button — deliberately left absolutely positioned,
       inheriting .task-photo-thumb-btn's own box wholesale (border/background/radius,
       and its 34x24 desktop / 48x40 touch sizing) rather than overriding position at
       all: that's the exact mechanism a real task's own photo thumbnail already uses to
       sit at a row's right edge without affecting that row's height (position:absolute
       takes it out of flex flow entirely) — reusing it here directly, instead of
       fighting it with a static override, is what makes "same height as ordinary task"
       and "similar style to that camera icon" both true at once. --photo-shift's own
       trigger selectors (make room for an overflow menu) never match this row — no
       .has-photo/.expanded here — so it stays put at the plain right:8px/16px inset. */
    .grocery-quick-add-open {
      display: flex; align-items: center; justify-content: center;
      font-size: 1rem; color: var(--muted);
    }
    .grocery-quick-add-open:hover { color: var(--accent); }
    /* Explicit height (matching .grocery-quick-add-btn's own 22.5px/26px, not padding
       alone) — this row's empty-state variant is the one case with two flex children,
       and padding-based sizing here came out a couple px taller than the button, enough
       to become the row's own tallest child and quietly break "same height as ordinary
       task" for that state specifically (found live, checked against a real task row
       rather than assumed fixed). */
    .grocery-quick-add-dismiss {
      flex-shrink: 0; height: 22.5px; display: flex; align-items: center; justify-content: center;
      background: none; border: none; color: var(--faint); cursor: pointer; padding: 0 6px;
    }
    .touch-mode .grocery-quick-add-dismiss { height: 26px; }
    .grocery-quick-add-dismiss:hover { color: var(--danger); }

    /* Medication tracker (2026-09-07) — one row per tracker (not per medication time),
       a compact strip of dots as its own visual. */
    .med-dots { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; flex-shrink: 0; }
    .med-dot {
      width: 12px; height: 12px; border-radius: 50%; box-sizing: border-box;
      border: 2px solid var(--border); background: transparent; transition: background .15s, border-color .15s;
    }
    .med-dot.taken, .med-dot.missed { border-color: transparent; }
    /* Matches .task-text's own size (2026-09-11 follow-up) — Malte: "unify the font
       size of the text [for section apps in the main view]... Potty log for example has
       bigger font than medication... make the font the same size as for ordinary
       tasks." Was .92rem, a size never actually decided on for this — just whatever the
       browser default happened to render at when this class was first added, same root
       cause potty-log-task-name had, just with an accidental override sitting on top of
       it instead of none at all. See that class's own comment and the shared
       touch-mode bump below for the rest of this pass. */
    .medication-task-name { font-size: .9375rem; }
    .medication-task-empty-hint { font-size: .82rem; color: var(--faint); flex-shrink: 0; }

    /* Setup/Edit dialog — same list-of-rows shape as .gemjar-reward-row, just a time
       input + a content input instead of emoji/label/cost. */
    .med-setup-list { display: flex; flex-direction: column; gap: 4px; }
    .med-setup-row {
      display: flex; align-items: center; gap: 6px;
      padding: 6px 8px; background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r);
    }
    .med-setup-row input[type=time].med-setup-time { flex-shrink: 0; width: 6.5em; padding: 6px 4px; }
    .med-setup-row input[type=text].med-setup-content { flex: 1; min-width: 0; }

    /* Today's checklist dialog — read-only on time/content, two tri-state buttons per
       row instead of a checkbox: "taken" and "missed" are mutually exclusive outcomes
       for one dose, not one on/off flag (2026-09-07 follow-up). */
    .med-checklist-list { display: flex; flex-direction: column; gap: 2px; }
    .med-checklist-row {
      display: flex; align-items: center; gap: 10px;
      padding: 8px; border-radius: var(--r);
    }
    .med-checklist-row:hover { background: rgba(0,0,0,.03); }
    .med-checklist-time { flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--faint); font-size: .88rem; }
    .med-checklist-content { flex: 1; min-width: 0; }
    .med-status-btn {
      flex-shrink: 0; background: none; border: none; cursor: pointer;
      font-size: 1.3rem; line-height: 1; padding: 4px; border-radius: 50%;
      color: #c7d0da; transition: color .12s;
    }
    .med-status-btn:hover { color: var(--faint); }
    .med-status-taken.active { color: #4caf50; }
    .med-status-missed.active { color: var(--danger); }

    /* History dialog (2026-09-08) — Malte: "is the history stored or just for the day?
       ... can you make it accessible somehow." It was stored (every day's log entries
       just accumulate in app_data.log, never read back past `todayUtcKey()` by anything
       in the UI) — this is what finally surfaces it, same real table-not-prose approach
       as Potty Log's own entry list, for the same reason: scanning a column beats
       re-reading a sentence per row when what matters is the pattern across days. */
    .med-history-table-wrap { overflow-x: auto; }
    .med-history-table { width: 100%; border-collapse: collapse; font-size: .78rem; white-space: nowrap; }
    .med-history-table th, .med-history-table td { padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border); }
    .med-history-table th {
      position: sticky; top: 0; background: var(--surface); color: var(--faint);
      font-weight: 600; text-transform: uppercase; font-size: .68rem; letter-spacing: .02em;
    }
    .med-history-table tbody tr:last-child td { border-bottom: none; }
    .med-history-table tbody tr:hover { background: rgba(0,0,0,.03); }
    .med-history-table td:nth-child(3) { white-space: normal; min-width: 8em; } /* Medication — the one column that should actually wrap */
    .med-history-status { display: flex; align-items: center; gap: 5px; }
    .med-history-status.taken { color: #4caf50; }
    .med-history-status.missed { color: var(--danger); }
    .med-history-empty { padding: 16px 8px; color: var(--faint); font-size: .85rem; text-align: center; }

    /* Habit tracker (2026-09-07, badge follow-up same day) — one row per tracker
       bundling any number of habits, same "one element, not one row per item" shape the
       medication tracker uses. One combined pill per type (2026-09-11 follow-up,
       replacing one badge per habit) — Malte: "once there are quite a few habits being
       tracked... it is hard to know what is what anyway if there are several ones, maybe
       combine the good habits into one pill and the bad habits into one. Maybe one
       leaf/flame and then middot separated numbers." True past one habit even before
       this: each badge only ever showed an icon+number, the habit's own label lived
       solely in its title tooltip, so telling same-type habits apart at a glance was
       never actually possible — one pill per type admits that instead of pretending
       otherwise, `.habit-badges` middot-joins every habit's own number inside it. Colors
       unchanged in spirit, just moved from the badge itself down to each number
       (`.habit-badge-num`) so a mixed pill (some done, some not) still reads correctly
       per habit: a bad habit's "days clean" streak (leaf, green normally/red the day
       it's broken) and a good habit's own streak (fire, matching buildStreakText's
       existing #F09C30 — Malte: "similar to the leaf but with the flame and the count"
       — vivid once done for the current period, muted while still pending). */
    /* Matches .task-text's own size (2026-09-11 follow-up) — see
       .medication-task-name's identical comment; same fix, same reasoning. */
    .habit-task-name { font-size: .9375rem; }
    .habit-badges { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; flex-shrink: 0; }
    .habit-badge {
      display: inline-flex; align-items: center; gap: 3px;
      padding: 2px 7px; border-radius: 10px; font-size: .72rem; font-weight: 600;
      font-variant-numeric: tabular-nums; line-height: 1.4;
    }
    .habit-badge i { font-size: .8rem; }
    .habit-badge-sep { opacity: .55; }
    .habit-badge-bad { background: rgba(76,175,80,.14); color: #3d9140; }
    .habit-badge-bad .habit-badge-num.broken { color: var(--danger); }
    .habit-badge-good { background: rgba(0,0,0,.05); color: var(--faint); }
    .habit-badge-good .habit-badge-num.done { color: #c97a1a; }

    /* Setup/Edit dialog — same list-of-rows shape as .med-setup-row, plus a Good/Bad
       type toggle and (good habits only) a Repeat… button opening the existing
       recurrence picker. */
    .habit-setup-row {
      display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
      padding: 6px 8px; background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r);
    }
    .habit-setup-row input[type=text].med-setup-content { flex: 1; min-width: 90px; }
    .habit-type-toggle { display: flex; flex-shrink: 0; border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
    .habit-type-btn {
      padding: 5px 9px; font-size: .78rem; border: none; cursor: pointer;
      background: var(--bg); color: var(--text);
    }
    .habit-type-btn.active { background: var(--accent); color: #fff; }
    .habit-repeat-btn {
      flex-shrink: 0; padding: 5px 9px; font-size: .78rem; border: 1px solid var(--border);
      border-radius: var(--r); background: var(--bg); color: var(--text); cursor: pointer;
    }
    .habit-repeat-fixed { flex-shrink: 0; font-size: .78rem; color: var(--faint); padding: 5px 4px; }

    /* Today's checklist dialog */
    .habit-checklist-list { display: flex; flex-direction: column; gap: 2px; }
    .habit-checklist-row { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--r); }
    .habit-checklist-row:hover { background: rgba(0,0,0,.03); }
    .habit-checklist-row input[type=checkbox] { flex-shrink: 0; width: 18px; height: 18px; cursor: pointer; }
    .habit-checklist-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
    .habit-checklist-label { font-size: .9rem; }
    .habit-checklist-streak { font-size: .76rem; color: var(--faint); }

    /* Potty Log (2026-09-07) — a voiding/bladder diary. Row badges are today's dry/
       accident tally; the dialog is a quick-add form (big tappable toggle/scale
       buttons, not dropdowns — meant to be usable in the moment) plus the full entry
       list grouped by day. */
    /* Never had a font-size rule of its own at all (2026-09-11 follow-up) — Malte:
       "Potty log for example has bigger font than medication." Every other section-app
       name class overrides the size explicitly; this one fell through to the plain 16px
       body default, visibly bigger than .task-text's own .9375rem (15px) sitting right
       above it in the same list. Matched here, same as every other kind's name class
       this same pass. */
    .potty-log-task-name { font-size: .9375rem; }
    .potty-log-badges { display: flex; gap: 5px; align-items: center; flex-shrink: 0; }
    .potty-log-badge {
      display: inline-flex; align-items: center; gap: 3px;
      padding: 2px 7px; border-radius: 10px; font-size: .72rem; font-weight: 600;
      font-variant-numeric: tabular-nums; line-height: 1.4;
    }
    .potty-log-badge i { font-size: .8rem; }
    .potty-log-badge-dry { background: rgba(76,175,80,.14); color: #3d9140; }
    .potty-log-badge-accident { background: rgba(66,133,244,.14); color: #3367d6; }

    .potty-log-form { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
    .potty-log-when-row { display: flex; gap: 6px; margin-bottom: 2px; }
    .potty-log-when-row input.potty-log-date { flex: 3; min-width: 0; }
    .potty-log-when-row input.potty-log-time { flex: 2; min-width: 0; }
    .potty-log-toggle-row { display: flex; gap: 6px; }
    .potty-log-toggle-btn {
      flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px;
      padding: 9px 6px; border-radius: var(--r); border: 1px solid var(--border);
      background: var(--bg); color: var(--text); font-size: .85rem; font-weight: 500; cursor: pointer;
    }
    .potty-log-toggle-btn.active { background: #4caf50; border-color: #4caf50; color: #fff; }
    .potty-log-toggle-btn.potty-log-toggle-accident.active { background: #4285f4; border-color: #4285f4; }
    .potty-log-scale-row { display: flex; gap: 5px; flex-wrap: wrap; }
    .potty-log-scale-btn {
      flex: 1; min-width: 5.5em; padding: 7px 4px; border-radius: var(--r);
      border: 1px solid var(--border); background: var(--bg); color: var(--text);
      font-size: .76rem; cursor: pointer; text-align: center;
      display: flex; align-items: center; justify-content: center; gap: 4px;
    }
    .potty-log-scale-btn i { font-size: 1rem; flex-shrink: 0; }
    .potty-log-table td i { font-size: 1.05rem; vertical-align: middle; }
    .potty-log-scale-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
    .potty-log-comment { margin-top: 4px; }

    /* Entry table (2026-09-07 follow-up) — a real <table>, not a card list, so columns
       actually line up for comparing across days at a glance. Horizontally scrollable
       on its own (not the whole dialog/page) once narrower than its content, same
       responsive-table convention used everywhere a wide grid meets a narrow screen.
       No own max-height/vertical scroll any more (2026-09-08, full-screen dialogs) —
       .dlg-body already scrolls the whole body (form + table together); a second,
       independently-capped scroll box nested inside it was two scrollbars competing for
       the same space. Horizontal overflow stays scoped to just this wrapper, not the
       whole body, so the form above it never shifts sideways along with a wide table. */
    .potty-log-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r); }
    .potty-log-table { width: 100%; border-collapse: collapse; font-size: .78rem; white-space: nowrap; }
    .potty-log-table th, .potty-log-table td { padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border); }
    .potty-log-table th {
      position: sticky; top: 0; background: var(--surface); color: var(--faint);
      font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
      white-space: nowrap;
    }
    .potty-log-table td:nth-child(7) { white-space: normal; min-width: 8em; } /* Comment — the one column that should actually wrap */
    .potty-log-table tbody tr:last-child td { border-bottom: none; }
    .potty-log-table tbody tr:hover { background: rgba(0,0,0,.03); }
    .potty-log-row-accident .potty-log-cell-outcome { color: #3367d6; font-weight: 600; }
    /* Currently-loaded-into-the-form-above row (2026-09-08, entry editing) — a plain
       persistent highlight, not a flash/transition, since the row can stay "being
       edited" for as long as the form above is open. */
    .potty-log-row-editing td { background: var(--accent-bg); }
    .potty-log-table .gemjar-reward-delete { padding: 2px 4px; font-size: .74rem; }

    /* Blood Pressure (2026-09-14) — row badge, quick-add number fields, and chart
       legend. The quick-add date/time row and the entry table both reuse Potty Log's
       own classes wholesale (identical layout intent); only what's genuinely different
       — four bare number fields instead of toggle buttons, and the chart legend — gets
       its own rules here. */
    .bp-task-name { font-size: .9375rem; }
    .bp-task-latest {
      font-size: .82rem; font-weight: 600; font-variant-numeric: tabular-nums;
      color: var(--text); flex-shrink: 0; display: flex; align-items: center; gap: 8px;
    }
    .bp-task-latest-pulse { font-weight: 500; color: var(--muted); display: inline-flex; align-items: center; gap: 2px; }
    .bp-task-latest-pulse i { font-size: .78rem; }

    .bp-num-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
    .bp-num-field { display: flex; flex-direction: column; gap: 2px; }
    .bp-num-field label {
      font-size: .66rem; font-weight: 700; color: var(--faint);
      text-transform: uppercase; letter-spacing: .04em;
    }
    .bp-num-field input { width: 100%; box-sizing: border-box; }
    /* Action cell (edit/delete), not a wrapping Comment cell — see this table's own
       comment in app.js for why it needs to override potty-log-table's 7th-column rule. */
    .bp-table td:nth-child(7) { white-space: nowrap; min-width: 0; }

    .bp-chart-caption { font-size: .76rem; font-weight: 600; color: var(--muted); padding: 10px 16px 0; }
    .bp-chart-legend { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; padding: 4px 16px 10px; }
    .bp-chart-legend-item { display: flex; align-items: center; gap: 6px; font-size: .72rem; color: var(--muted); font-weight: 600; }
    .bp-chart-legend-swatch { width: 18px; height: 0; border-top: 2px solid var(--text); display: inline-block; }
    .bp-chart-legend-swatch.dotted { border-top-style: dotted; border-top-width: 3px; }
    .bp-chart-legend-swatch.dashed { border-top-style: dashed; }
    .bp-chart-legend-swatch.mad { border-top-width: 3px; }

    /* empty state */
    .empty-hint { display:flex; flex-direction:row; align-items:center; justify-content:center; gap:10px; padding:0 16px; height:48px; box-sizing:border-box; cursor:pointer; overflow:hidden; }
    .empty-hint:hover { background:rgba(0,0,0,.03); }
    .empty-hint i { font-size:1.4rem; color:var(--faint); opacity:.6; flex-shrink:0; }
    .empty-hint-title { font-size:.84rem; font-weight:600; color:var(--faint); }

    /* quick-add input below a collapsed section header */
    .pending-new-task {
      border: 1.5px solid var(--accent); border-radius: var(--r);
      padding: 8px 12px; font-size: .87rem; font-family: inherit;
      background: #fff; outline: none; color: var(--text);
      width: 100%; display: block; margin-top: 2px;
      box-shadow: 0 0 0 3px rgba(98,112,182,.15);
    }

    /* between-task gap */
    .task-gap {
      height: 16px; position: relative; display: flex; align-items: center;
      margin: -8px 0; cursor: pointer; z-index: 11;
    }
    .task-gap:hover { z-index: 15; }
    .task-gap::after {
      content: ''; position: absolute; left: 0; right: 0; top: 50%;
      height: 2px; background: var(--gap-color, #6b7280); opacity: 0; transition: opacity .4s;
      border-radius: 1px; pointer-events: none;
    }
    .task-gap:hover::after { opacity: 1; }
    .gap-plus {
      flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%;
      background: var(--gap-color, #6b7280); color: #fff; border: none;
      font-size: 14px; line-height: 1; pointer-events: none;
      display: flex; align-items: center; justify-content: center;
      position: relative; z-index: 1; padding: 0;
      opacity: 0; visibility: hidden; transition: opacity .4s, visibility .4s;
    }
    .task-gap:hover .gap-plus { opacity: 1; visibility: visible; }
    .task-gap.active { height: auto; padding: 3px 0 4px; margin: 0; cursor: default; background: #fff; }
    .task-gap.active::after { display: none; }
    /* drop indicator during new-task drag */
    .task-gap.drop-here { height: 4px !important; margin: 0 !important; background: var(--accent); border-radius: 2px; }
    .task-gap.drop-here::after, .task-gap.drop-here .gap-plus { display: none !important; }
    .gap-input {
      flex: 1; border: none; border-bottom: 1.5px solid var(--accent);
      padding: 3px 2px; font-size: .87rem; background: transparent;
      outline: none; color: var(--text); margin-left: 4px;
    }
    .gap-input::placeholder { color: #b8cad8; }
    /* .gap-photo removed (2026-09-15) — the draft card's inline camera icon it styled is
       gone; Media lives only in the task action bar now, on every pointer type (see
       renderGap's own comment). */

    /* disable interfering elements while dragging */
    .dragging-task .empty-hint,
    .dragging-new .empty-hint { pointer-events: none; }
    .tasks.drag-target { outline: 2px dashed var(--accent-bg); outline-offset: 4px; border-radius: var(--r); }
    .section.drop-new-section .sec-head {
      background: var(--accent-bg); border-radius: var(--r);
      border-bottom-color: var(--accent);
    }

    /* drag state */
    .dragging { opacity: .3; box-shadow: none !important; }
    .drop-above { outline: 2px solid var(--accent) !important; outline-offset: 2px; }
    .drop-below { outline: 2px solid var(--accent) !important; outline-offset: 2px; }
    /* sections still use drop-above/below for reordering */

    /* section icon picker */
    .sec-icon-display {
      flex-shrink: 0; display: flex; align-items: center; font-size: 1.4rem;
    }
    .sec-icon-btn {
      flex-shrink: 0; background: none; border: none; cursor: pointer;
      width: 28px; height: 28px; border-radius: 6px;
      display: flex; align-items: center; justify-content: center;
      font-size: 1.1rem; transition: background .12s;
    }
    .sec-icon-btn:hover { background: rgba(0,0,0,.07); }
    .sec-icon-add { color: var(--faint); }

    /* section edit modal */
    .sec-edit-overlay {
      /* 10100, above the custom tooltip/toast (10001) — backlog #12's "the focus is over
         the dialog" report happened right when tapping the photo view button, and those
         are the only two things in the app with a higher z-index than a dialog's old 9000;
         either lingering on-screen at that moment would explain it exactly (both have
         pointer-events:none, so this is purely visual — a tooltip/toast rendering over a
         freshly-opened dialog is backwards either way, dialog should always win). Best
         guess without a way to see the actual repro (2026-09-01) — flag if it recurs. */
      position: fixed; inset: 0; z-index: 10100;
      background: rgba(0,0,0,.25); display: flex; align-items: center; justify-content: center;
      padding: 16px 0; box-sizing: border-box;
    }
    .sec-edit-modal {
      background: var(--surface); border-radius: 12px;
      box-shadow: 0 12px 40px rgba(0,0,0,.2);
      padding: 20px; width: min(360px, calc(100vw - 32px));
      display: flex; flex-direction: column; gap: 14px;
      /* Icon grid + colors + areas can add up to taller than a keyboard-shrunk mobile
         viewport (interactive-widget=resizes-content shrinks the layout viewport itself,
         same mechanism the app shell relies on elsewhere) — cap and scroll internally
         instead of letting the modal overflow off-screen with no way to reach the rest. */
      max-height: calc(100vh - 32px);
      max-height: calc(100dvh - 32px);
      overflow-y: auto;
    }
    .sec-edit-modal h3 { font-size: .95rem; font-weight: 700; margin: 0; }
    .sec-edit-modal input[type=text], .sec-edit-modal input[type=number], .sec-edit-modal input[type=date],
    .sec-edit-modal input[type=time], .sec-edit-modal select, .sec-edit-modal textarea {
      width: 100%; box-sizing: border-box;
      padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--r);
      font-size: .9rem; font-family: inherit; background: var(--bg); color: var(--text);
      outline: none;
    }
    .sec-edit-modal textarea { resize: vertical; }
    .sec-edit-modal input[type=text]:focus, .sec-edit-modal input[type=number]:focus, .sec-edit-modal input[type=date]:focus,
    .sec-edit-modal input[type=time]:focus, .sec-edit-modal select:focus, .sec-edit-modal textarea:focus { border-color: var(--accent); }
    /* date/time inputs render their own OS-native picker icon regardless of the
       surrounding theme, which is fine — but that icon defaults to dark-on-transparent
       and disappears against this app's own dark-mode background without a hint that
       the field should render light-on-dark. color-scheme is exactly that hint. */
    .sec-edit-modal input[type=date], .sec-edit-modal input[type=time] { color-scheme: light dark; }
    .sec-edit-modal-label { font-size: .72rem; font-weight: 600; color: var(--faint); text-transform: uppercase; letter-spacing: .05em; }
    .sec-edit-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }

    /* Full-screen dialog shell (2026-09-08) — promoted from the Dialog Lab
       exploration (Malte: "Let's try the full screen version... I think this
       might make it easier to give individual designs to section apps and maybe
       feels more premium") to a permanent shell, now used by every section-app
       "view" dialog (medication/habit/metric/gemjar/potty-log/phrase) and the
       ordinary task's own "Open in dialog…". Each kind's own setup/edit dialog
       (Edit times…, Edit habits…, etc.) deliberately stays the old centered-card
       look for now — Malte's own scoping call, not every dialog in the app.
       .dlg-header/.dlg-body/.dlg-footer is the shared "header and footer
       actually fixed, only the middle scrolls" structure — the whole reason
       this exists at all (see the modal's own base rule for the bug this
       replaces: one scrolling box for everything, footer included).
       The overlay/modal both carry the real .sec-edit-overlay/.sec-edit-modal
       classes plus this .dlg-fullscreen modifier, not a separate lookalike
       pair — so window.handleNativeBackPress()'s `.sec-edit-overlay,
       .modal-bg` query (and anything else already keyed off those classes)
       picks up every fullscreen dialog for free, same fix the Lab needed for
       exactly the same reason. */
    .dlg-header, .dlg-footer { flex-shrink: 0; }
    /* The header's own title used to be a plain .dlg-header h3 with its own truncation
       rule here (2026-09-11, openTaskReminderDialog's header switched from the section
       name to the task's own text — "use the actual title of the checklist item. And if
       it is too long there truncate it.") — now built by the shared buildAppHeader as a
       .sec-title instead (same header-consolidation pass, "one great default for that
       extra dialog window"), which already truncates on its own (see .sec-title above),
       so no dialog-specific override is needed here any more. */
    .dlg-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; }
    .dlg-footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--border); }

    .sec-edit-overlay.dlg-fullscreen { background: var(--surface); padding: 0; }
    .sec-edit-modal.dlg-fullscreen {
      display: flex; flex-direction: column; width: 100%; height: 100%; height: 100dvh;
      max-height: none; border-radius: 0; box-shadow: none; padding: 0; gap: 0; overflow: hidden;
    }
    /* Padding tuned to land this header at the exact same total height as the app's own
       top bar/Focus Mode header (.top-bar-inner) — Malte: "Can you make the full dialog
       header the same height as the app header and focus mode header? Same applies for
       the section app header's height please." (every .dlg-fullscreen dialog shares this
       one rule, so both asks are the same fix.) .top-bar-inner is 64px at its own base
       size, 56px under `@media (pointer:fine)` (a mouse/trackpad) — matched here via the
       identical query, not the .touch-mode JS class, so this can never drift out of sync
       with whatever actually changed .icon-btn-top's own size in the first place. Numbers
       aren't guessed: measured live (getBoundingClientRect) — .icon-btn-top is 48px at
       its own base size / 34px under pointer:fine, plus this header's own 1px
       border-bottom, leaves a 15px pointer:coarse / 21px pointer:fine budget for
       top+bottom padding combined to land on 64px/56px exactly. */
    .dlg-fullscreen .dlg-header {
      padding: 8px 16px 7px; padding-top: calc(8px + env(safe-area-inset-top));
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: 10px;
      /* position:relative + space-between added 2026-09-11 (header-consolidation) —
         mirrors .top-bar-inner exactly now that both are populated by the shared
         buildAppHeader: relative anchors its absolutely-centered .app-header-center,
         space-between pushes the back button and optional menu button to the two edges
         without needing .top-bar-left/.top-bar-right-style wrapper divs in here. Keep
         this in sync with .top-bar-inner's own identical addition above. */
      position: relative; justify-content: space-between;
    }
    @media (pointer: fine) {
      .dlg-fullscreen .dlg-header { padding: 11px 16px 10px; padding-top: calc(11px + env(safe-area-inset-top)); }
    }
    /* Bottom padding accounts for safe-area even without a footer below it (2026-09-09,
       section-app dialogs dropped their own Close button — see app.js's own comment on
       why; openTaskReminderDialog followed suit the same day) — .dlg-body is now always
       the bottom-most element in every .dlg-fullscreen dialog, where a footer used to be
       the one carrying this. .dlg-fullscreen .dlg-footer below is unused dead weight
       until some future dialog in this shell actually needs one again — left in rather
       than removed, since the rule costs nothing sitting idle. */
    .dlg-fullscreen .dlg-body { padding: 16px 16px calc(16px + env(safe-area-inset-bottom)) 16px; }
    .dlg-fullscreen .dlg-footer { padding: 14px 16px; padding-bottom: calc(14px + env(safe-area-inset-bottom)); }

    /* On a roomy viewport, .dlg-fullscreen drops the edge-to-edge treatment for a
       floating card instead (2026-09-11) — Malte: "when the screen is wider than maybe
       360px and taller than 600px, can you make the full screen dialog into a dialog
       that just blends the background away a bit." Only the outer shape changes
       (dimmed backdrop instead of solid full-bleed, bounded width/height, rounded
       corners, a real shadow) — .dlg-header/.dlg-body/.dlg-footer's own layout is
       untouched, so every dialog already built on this shell (every section-app view
       dialog, the ordinary task's own "Open in dialog…") gets this for free, nothing
       dialog-specific to update. Widened past the plain .sec-edit-modal's own 360px cap
       (min(480px, ...) here) since these carry richer content — a photo, a task's full
       field row, a medication checklist — than that shell's small edit forms.
       Desktop-only, `body:not(.touch-mode)` (2026-09-11 follow-up) — Malte: "The floating
       dialog only for desktop mode please - not for touch. That should stay full
       screen." A touch device with a roomy screen (a tablet, mainly) still gets true
       full-screen regardless of how comfortably it clears the size threshold below —
       size alone was the wrong signal, input type is what actually matters here. */
    @media (min-width: 360px) and (min-height: 600px) {
      body:not(.touch-mode) .sec-edit-overlay.dlg-fullscreen { background: rgba(0,0,0,.25); padding: 16px 0; }
      body:not(.touch-mode) .sec-edit-modal.dlg-fullscreen {
        width: min(480px, calc(100vw - 32px)); height: auto;
        max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px);
        border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,.2);
      }
    }

    /* Desktop wide mode (2026-09-13) — Malte: "Can we implement a Desktop wide mode with
       the full screen dialog now taking up everything on the right side and opening
       instead of tasks collapsing and expanding." Placed directly after the floating-card
       breakpoint above, on purpose: same selectors, same specificity (one class-pair
       each), so file order alone decides the winner per property at this wider range —
       every property the floating rule sets above is re-declared here, so nothing leaks
       through for a wide-but-short window (e.g. 900x500, which fails that rule's own
       min-height:600px but still needs to dock cleanly under this one). Width-only gate,
       deliberately not paired with a min-height the way the rule above is — a short wide
       window should still dock, there's no floating card here to need vertical room for.
       body:not(.touch-mode) matches that same rule's own gating, for the same reason
       ("The floating dialog only for desktop mode please - not for touch"). */
    /* Real bug, found live (2026-09-13 follow-up) — Malte: "maybe the problem is rather
       the 'Select a task to see its details' is sub 600px also visible at the very
       bottom. So there is an additional scrollbar currently." Confirmed exactly that:
       #wide-pane-empty only ever got `display:flex` *inside* the wide-mode media query
       below — nothing set its default state outside it, so under 600px it fell back to a
       plain block-level <div>, sitting in normal document flow at the bottom of body with
       its own text content, stretching the page taller than the viewport and forcing a
       second scrollbar alongside .scroll-region's own. This base rule is the actual fix;
       the media query below still overrides it back to flex at >=600px as before. */
    #wide-pane-empty { display: none; }

    @media (min-width: 600px) {
      /* One clamp() is the whole "300px, scaling up to a 360px cap" ask — 50vw resolves
         to exactly 300px at a 600px viewport and exactly 360px at 720px, so the formula
         needs no separate tuning beyond the clamp() call itself. Defined once as a custom
         property (not repeated as a literal in every rule below) so the left-pane width
         and the right-pane's own left offset can never drift out of sync with each other. */
      body:not(.touch-mode) { --wide-left-w: clamp(300px, 50vw, 360px); }
      body:not(.touch-mode) .wrap { width: var(--wide-left-w); }

      /* Right pane: full-bleed within its own bounds (not the whole viewport, not
         centered/floating) — the left pane stays visible and interactive beside it, so no
         dimmed backdrop either; the whole reason the app splits at this width. */
      body:not(.touch-mode) .sec-edit-overlay.dlg-fullscreen {
        position: fixed; left: var(--wide-left-w); right: 0; top: 0; bottom: 0;
        background: var(--surface); padding: 0;
      }
      body:not(.touch-mode) .sec-edit-modal.dlg-fullscreen {
        width: 100%; height: 100%; max-height: none; border-radius: 0; box-shadow: none;
      }
      /* Content only, not the header (2026-09-13 follow-up) — Malte: "Content (not the
         header) on the right side max. 720px incl. padding. For title, notes, media,
         schedule." Capped the same max-width:720px .scroll-inner/.top-bar-inner already
         use for the *left* pane's own content, applied here to .dlg-body specifically so
         a very wide right pane doesn't stretch a task's title/notes/media/schedule edge
         to edge, while .dlg-header (the back arrow + "⋯" row) still spans the pane's full
         width as it already does. box-sizing:border-box is already the sheet's own global
         reset, so 720px already includes .dlg-body's own padding, no extra math needed.
         Left-aligned, not centered (2026-09-13 follow-up) — .scroll-inner/.top-bar-inner's
         own margin:0 auto made sense there (that column sits in the middle of the whole
         viewport with no other content beside it); this one sits right up against the
         left pane already, so centering it just opened uneven gaps on both sides instead
         of reading as one continuous flow from the list into its own detail. */
      body:not(.touch-mode) .dlg-fullscreen .dlg-body { max-width: 720px; width: 100%; }

      /* Right pane's own resting state — Malte: "always split, empty placeholder by
         default," not single-pane until something's opened. Built once in render() (see
         app.js), a plain document.body sibling like every dialog already is. Hidden
         via :has() whenever any dlg-fullscreen dialog is actually present — the same
         :has() pattern already proven live elsewhere in this file (the shopping-mode
         "no photo yet" button's own reveal-on-edit rule) rather than new JS to track it. */
      body:not(.touch-mode) #wide-pane-empty {
        display: flex; position: fixed; left: var(--wide-left-w); right: 0; top: 0; bottom: 0;
        align-items: center; justify-content: center; color: var(--faint); font-size: .9rem;
        padding: 0 32px; text-align: center;
      }
      body:not(.touch-mode):has(.sec-edit-overlay.dlg-fullscreen) #wide-pane-empty { display: none; }

      /* A brief same-day experiment re-anchored .task-action-bar to the left pane at
         this width (2026-09-15), for when wide mode tried showing the bar alongside its
         own dialog — reverted along with that (Malte, after testing live: "remove the
         floating bar on the left for wide screens - doesn't make sense in that setup"),
         so the bar is narrow-desktop/touch-only again and never reaches this width in
         the first place; no override needed here any more. */
    }

    /* Ordinary task's full-screen dialog body (2026-09-09 follow-up) — its own dedicated
       layout (buildTaskDetailBody() in app.js), not the list row's own .task/.task-row
       reused inside a dialog. That first attempt (shipped, then reverted the same day)
       is exactly why these are their own classes: .task-row's own padding stacked with
       .dlg-body's (Malte: "The extra padding is also weird"), and this view has no more-
       collapsed state to fold back down to, so nothing here needs a collapse control the
       way the list row's own note area does. */
    .task-detail-title-row { display: flex; align-items: flex-start; gap: 10px; }
    .task-detail-title-row .task-check { margin-top: 3px; }
    .task-detail-title {
      flex: 1; min-width: 0; font-size: 1.05rem; font-weight: 700; line-height: 1.4;
      color: var(--text); word-break: break-word; outline: none; border-radius: 3px;
    }
    /* No visible focus outline while editing (2026-09-12) — see .task-text:focus's own
       comment; same task-title-editing concept, just the full-screen dialog's own copy. */
    .task-detail-title:focus { outline: none; }
    /* Below the title, bigger than a thumbnail, at a fixed banner-style ratio rather than
       shown at its own natural size (2026-09-09 follow-up) — Malte: "The photo ... Should
       be below the title but displayed bigger - maybe full width, but cropped to a
       preview 16:9 or 3:4 ratio." See buildTaskDetailBody's own comment on why 16:9 over
       3:4. No longer actually *cropped* (2026-09-13 follow-up) — Malte: "I would rather
       see the whole picture centered - keep the height that area currently has. So no
       cropping, just shrinking to fit and position it centered." The box itself (width,
       16:9 aspect-ratio) is untouched — only the image inside changed from object-fit:
       cover to contain, which centers by default (object-position's own initial value),
       so nothing else needed for that part. A plainer, more visibly distinct fill instead
       of var(--bg) (2026-09-13 follow-up) — Malte: "add some background for that picture
       area" — var(--bg) was already there, but it sits so close to this dialog's own
       white body (var(--surface)) that it read as no background at all; this one's
       actually meant to show, in every letterboxed gap a non-16:9 photo now leaves. */
    .task-detail-photo {
      display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: var(--r);
      overflow: hidden; border: none; padding: 0; margin: 0; cursor: pointer;
      background: #eef1f6;
    }
    .task-detail-photo img { width: 100%; height: 100%; object-fit: contain; display: block; }
    /* Wraps .task-detail-photo so a delete button can sit as its own sibling, absolutely
       positioned over the image, rather than nested inside it (2026-09-11 follow-up) —
       Malte: "on the photo add a trash can icon so it can easily be removed." A <button>
       can't contain another <button> validly, and nesting it inside .task-detail-photo
       would also trigger that button's own onclick (open the lightbox) on every tap. */
    .task-detail-photo-wrap { position: relative; }
    .task-detail-photo-delete {
      position: absolute; top: 8px; right: 8px; width: 32px; height: 32px;
      border-radius: 50%; border: none; cursor: pointer;
      background: rgba(0,0,0,.45); color: #fff;
      display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
      transition: background .12s;
    }
    .task-detail-photo-delete:hover { background: rgba(0,0,0,.65); }
    /* .task-tags already has its own top/bottom padding (tuned for sitting inside the
       list row's .task-note-area) — zeroed here since this row is a direct .dlg-body
       child instead, already evenly spaced from its siblings by .dlg-body's own flex
       gap; keeping .task-tags' padding too would've doubled up right where Malte flagged
       the spacing as "weird." */
    .task-detail-fields { padding: 0; }
    /* .task-recurrence-info's own 16px horizontal padding assumes it's sitting inside a
       list row that has none of its own (2026-09-10 follow-up) — reused as-is in
       buildTaskDetailBody() for the "Done X×"/next/before info, but .dlg-body already
       carries that same 16px on this dialog specifically, so left unzeroed it would
       double up exactly the way Malte already flagged once for .task-tags here. */
    .task-detail-body .task-recurrence-info { padding-left: 0; padding-right: 0; }
    /* Groups the due-date/repeat pill (or "+ Schedule") with the repeating-history lines
       just below it into one visual block (2026-09-11 reorder) — see
       buildTaskDetailBody's own top comment for why they share a section instead of
       getting a divider between them. A small gap of its own for the space between the
       pill row and the first history line, which otherwise has nothing (.task-tags'
       padding is zeroed above, and .task-recurrence-info only pads its own bottom, not
       top) — .task-recurrence-info's existing bottom padding still handles the rest. */
    .task-detail-due-section { display: flex; flex-direction: column; gap: 4px; }
    /* One thin line between each block of buildTaskDetailBody (2026-09-11) — Malte:
       "Each gets a new line. Maybe separated with a thin grey divider." Same color/
       thickness as the app's other divider (.menu-divider) but no margin of its own —
       .dlg-body's own flex gap (14px) already spaces it from its neighbors on both
       sides, adding more here would just double it up. */
    .task-detail-divider { height: 1px; background: var(--border); flex-shrink: 0; }
    /* Leading Phosphor icon per section, aligned to the title row's own checkbox column
       (2026-09-11) — Malte: "In order to make the design easier on the eyes... add a
       phosphor icon for each section: media, note, schedule/calendar." Same 20px width
       and gap as .task-detail-title-row's own checkbox+text pairing, so every row's
       content lines up in one consistent left column regardless of whether that row
       starts with a checkbox or an icon. margin-top nudges the icon's own optical
       center down to match a text/button row's cap-height, the same purpose
       .task-detail-title-row .task-check's own margin-top already serves for the
       checkbox specifically. */
    .task-detail-icon-row { display: flex; align-items: flex-start; gap: 10px; }
    /* Content column always fills the row's remaining width, explicitly rather than
       incidentally (2026-09-11 follow-up, added while giving the photo banner this same
       icon row — see mkIconRow's own JS comment). Text content (notes/due/media-empty)
       happened to already look right without this — a wrapping line of text is wide
       enough on its own that nothing exposed the gap — but the photo banner has no
       intrinsic width of its own once its `width:100%` chain is followed all the way
       down to a shrink-to-fit flex item with nothing else driving its size, which would
       collapse instead of actually spanning the row. min-width:0 lets long unbroken
       content (a URL in the note view, say) actually wrap/scroll within the column
       instead of forcing the row wider than the dialog itself. */
    .task-detail-icon-row > i.task-detail-row-icon + * { flex: 1; min-width: 0; }
    /* font-size bumped to match .task-check's own 20px box directly (2026-09-11 follow-
       up) — Malte: "Make the icons similar in size to the checkbox." 1rem/16px (this
       rule's own original size) read visibly smaller side by side with a 20px checkbox.
       `i.` prefix (not just `.task-detail-row-icon`), needed to actually win: the shared
       icon-font base rule up top, `i[class^="ph"] { font-size: 1em; ... }`, has a higher
       specificity (element + attribute selector) than a bare single class, so it was
       silently overriding a first attempt here that used the plain class alone —
       confirmed directly (iterating document.styleSheets' own matched rules) rather than
       guessed at from a size that still looked wrong after the "fix." */
    i.task-detail-row-icon {
      width: 20px; flex-shrink: 0; font-size: 1.5rem; line-height: 20px;
      text-align: center; color: var(--faint); margin-top: 2px;
    }
    /* Base chrome lives on the classes themselves, not ".sec-edit-footer button" — these
       two buttons get reused outside a footer now (the photo editor's source-picker and
       grid-size rows), and a container-scoped selector silently left them looking like
       unstyled native buttons there (2026-08-28, reported as "look nothing like our other
       buttons"). */
    .sec-edit-cancel, .sec-edit-save {
      padding: 7px 16px; border-radius: var(--r); border: none; cursor: pointer;
      font-size: .85rem; font-family: inherit; font-weight: 500;
    }
    .sec-edit-cancel { background: var(--bg); color: var(--text); }
    .sec-edit-cancel:hover { background: var(--border); }
    .sec-edit-cancel:disabled { opacity: .5; cursor: not-allowed; }
    .sec-edit-cancel:disabled:hover { background: var(--bg); }
    .sec-edit-save { background: var(--accent); color: #fff; }
    .sec-edit-save:hover { background: var(--accent2); }
    .sec-edit-save:disabled { opacity: .5; cursor: not-allowed; }
    .sec-edit-save:disabled:hover { background: var(--accent); }
    /* Section-apps picker: one clickable example card per type (2026-08-28), replacing
       a plain <select> — picking a type is now a visual "here's what this looks like"
       choice instead of reading a list of names. */
    .app-type-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
    .app-type-card {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 8px; min-height: 88px; padding: 16px 10px;
      border: 1px solid var(--border); border-radius: var(--r); background: var(--bg);
      cursor: pointer; font-family: inherit;
    }
    .app-type-card:hover { border-color: var(--accent); background: var(--accent-bg); }
    .app-type-card-icon { font-size: 1.6rem; color: var(--accent); }
    .app-type-card-preview { line-height: 0; }
    .app-type-card-label { font-size: .85rem; font-weight: 600; color: var(--text); }
    /* Step 2 (name/section/setup) title row: a "← Back" to the card grid, since picking
       the wrong type shouldn't mean closing the whole picker and starting over. */
    .app-details-title-row { display: flex; align-items: center; gap: 8px; }
    /* Move-task dialog: a scoped, capped-height list inside the section-editor-style
       modal — the dropdown version of this (one row per section) got unwieldy once
       there were enough sections to need scrolling inside an already-small menu. */
    .move-task-list {
      max-height: 45vh; overflow-x: hidden; overflow-y: auto;
      border: 1px solid var(--border); border-radius: var(--r);
      display: flex; flex-direction: column;
    }
    .move-task-row { border-radius: 0; }
    .area-picker-chips { display: flex; flex-wrap: wrap; gap: 6px; }
    .area-picker-add-row { display: flex; gap: 8px; }
    .area-picker-add-row input[type=text] { flex: 1; width: auto; }
    .area-picker-add-row button { flex-shrink: 0; }
    /* Section editor's collapsible "Icon & background" disclosure — keeps a brand-new
       section's creation to just a title by default, without losing the full picker
       for anyone who wants it (or is editing a section that already uses one). */
    .sec-edit-advanced-toggle {
      display: flex; align-items: center; justify-content: space-between;
      width: 100%; box-sizing: border-box; background: none; cursor: pointer;
      border: none; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
      padding: 9px 2px; font-size: .78rem; font-weight: 600; color: var(--muted);
      font-family: inherit;
    }
    .sec-edit-advanced-toggle:hover { color: var(--text); }
    .sec-edit-advanced-toggle i { font-size: .78rem; }
    .sec-edit-advanced { display: flex; flex-direction: column; gap: 14px; }

    .icon-picker {
      position: absolute; top: calc(100% + 6px); left: 0; z-index: 200;
      background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r); box-shadow: 0 8px 28px rgba(0,0,0,.14);
      padding: 10px; width: 300px;
    }
    .icon-picker-grid {
      display: grid; grid-template-columns: repeat(9, 1fr); gap: 2px; margin-bottom: 8px;
    }
    .icon-picker-grid button {
      background: none; border: none; cursor: pointer; border-radius: 5px;
      font-size: 1.1rem; width: 100%; aspect-ratio: 1;
      display: flex; align-items: center; justify-content: center;
      transition: background .1s; color: var(--text);
    }
    .icon-picker-grid button:hover { background: var(--igrid-hover, var(--accent-bg)); }
    .icon-picker-grid button.selected { background: var(--igrid-sel-bg, var(--accent-bg)); outline: 2px solid var(--igrid-sel-color, var(--accent)); outline-offset: -2px; }
    .icon-picker-colors {
      display: grid; grid-template-columns: repeat(9, 1fr); gap: 5px; margin-bottom: 8px;
    }
    .icon-picker-colors button {
      width: 100%; aspect-ratio: 1; border-radius: 50%; border: 2px solid transparent;
      cursor: pointer; transition: border-color .1s; box-shadow: 0 1px 3px rgba(0,0,0,.2);
    }
    .icon-picker-colors button.selected { border-color: var(--text); }
    .icon-picker-colors button[style*="#ffffff"] { box-shadow: 0 0 0 1.5px #d1d5db; }
    .icon-picker-section-label {
      font-size: .72rem; font-weight: 600; color: var(--faint);
      text-transform: uppercase; letter-spacing: .05em;
      margin: 8px 0 5px;
    }
    .icon-picker-bg-colors {
      display: grid; grid-template-columns: repeat(9, 1fr); gap: 5px; margin-bottom: 8px;
    }
    .icon-picker-bg-colors button {
      width: 100%; aspect-ratio: 1; border-radius: 4px; border: 2px solid transparent;
      cursor: pointer; transition: border-color .1s;
    }
    .icon-picker-bg-colors button.selected { border-color: var(--text); }
    .icon-picker-remove {
      width: 100%; padding: 5px; font-size: .8rem; border: none; border-radius: 5px;
      background: var(--danger-bg); color: var(--danger); cursor: pointer;
    }
    .icon-picker-remove:hover { background: #f9c5c7; }

    /* modal */
    .modal-bg {
      position: fixed; inset: 0; background: rgba(0,0,0,.4);
      display: flex; align-items: center; justify-content: center; z-index: 200;
    }
    .modal {
      background: var(--surface); border-radius: 14px; padding: 24px;
      width: 580px; max-width: 95vw; display: flex; flex-direction: column; gap: 12px;
      box-shadow: 0 24px 64px rgba(0,0,0,.22);
    }
    .modal h2 { font-size: 1rem; font-weight: 700; }
    .modal textarea { min-height: 320px; font-family: monospace; font-size: .8rem; }
    .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }

    /* new task dialog */
    .new-task-overlay {
      position: fixed; inset: 0; z-index: 8000;
      background: rgba(20,30,60,.35);
      display: flex; align-items: flex-start;
      justify-content: center; padding-top: 80px;
    }
    .new-task-card {
      background: var(--surface); border-radius: 10px;
      box-shadow: 0 8px 32px rgba(30,50,100,.2), 0 0 0 1px rgba(30,50,100,.07);
      width: 100%; max-width: 480px; margin: 0 16px;
      overflow: hidden;
    }
    .new-task-sec-row {
      display: flex; align-items: center; gap: 6px;
      padding: 10px 14px 8px; border-bottom: 1px solid var(--border);
      font-size: .78rem; color: var(--faint);
    }
    .new-task-sec-row select {
      border: none; background: transparent; color: var(--muted);
      font-size: .78rem; font-family: inherit; outline: none; cursor: pointer;
    }
    .new-task-title {
      display: block; width: 100%; border: none; outline: none;
      background: transparent; font-size: .95rem; font-family: inherit;
      padding: 12px 14px 4px; color: var(--text);
    }
    .new-task-note {
      display: block; width: 100%; border: none; outline: none; resize: none;
      background: transparent; font-size: .85rem; font-family: inherit;
      padding: 0 14px 6px; color: var(--text); overflow: hidden; line-height: 1.6;
    }
    /* search bar */
    .search-bar {
      position: sticky; top: 0; z-index: 40;
      padding: 8px 20px 0;
      margin: 0 auto 24px; max-width: 720px;
    }
    .search-bar-inner {
      display: flex; align-items: center; gap: 8px;
      background: var(--surface); border: 1.5px solid var(--accent);
      border-radius: var(--r); padding: 6px 10px;
      box-shadow: 0 2px 12px rgba(98,112,182,.15);
    }
    .search-bar-inner i { color: var(--accent); font-size: 1rem; flex-shrink: 0; }
    .search-bar-inner input {
      flex: 1; border: none; outline: none; background: transparent;
      font-size: .9rem; color: var(--text); font-family: inherit;
    }
    .search-bar-inner input::placeholder { color: var(--faint); }
    .search-clear { background: none; border: none; cursor: pointer; color: var(--faint);
      display: flex; align-items: center; justify-content: center;
      width: 20px; height: 20px; border-radius: 4px; flex-shrink: 0; }
    .search-clear:hover { color: var(--text); background: rgba(0,0,0,.07); }
    .section.search-no-match { opacity: 0.25; pointer-events: none; }
    .section.search-no-match .sec-head { pointer-events: none; }
    mark { background: #fef08a; color: inherit; border-radius: 2px; padding: 0 1px; }
    @media (min-width: 860px) {
      .search-bar { top: 0; padding: 20px 20px 0; }
    }
    @keyframes spark-fly {
      0%   { transform: translate(0,0) scale(1) rotate(0deg); opacity: 1; }
      60%  { opacity: 1; }
      100% { transform: translate(var(--tx),var(--ty)) scale(0) rotate(var(--rot)); opacity: 0; }
    }
    @keyframes ring-pop {
      0%   { transform: translate(-50%,-50%) scale(.3); opacity: .6; }
      100% { transform: translate(-50%,-50%) scale(2.8); opacity: 0; }
    }
    .spark {
      position: fixed; pointer-events: none; z-index: 9999;
      animation: spark-fly var(--dur) var(--delay) cubic-bezier(.15,.8,.35,1) both;
    }
    .spark-ring {
      position: fixed; pointer-events: none; z-index: 9998; border-radius: 50%;
      border: 2.5px solid var(--rc);
      animation: ring-pop .5s ease-out forwards;
    }

    /* ── Touch mode ──────────────────────────────────────────────────────── */
    /* Bigger tap targets. Right padding 16px, not 4 (2026-09-11 follow-up, second pass
       same day) — Malte first asked the five section-app kinds to match an ordinary
       task's own photo-thumbnail baseline (8px), tried 16px for just those five instead
       ("instead of 8px, can you try 16px on mobile as well"), then asked for the same
       16px universally: "Padding 16px also for ordinary task incl. photos." Since every
       row already shares this one rule, raising it here directly is the fix — the old
       per-kind touch override that used to bump just those five kinds up to 16px is
       gone, made redundant by this now computing the same value for free. See
       .touch-mode .task-photo-thumb-btn and .touch-mode .has-photo .task-text-wrap just
       below for the photo thumbnail's own matching move — a plain checkbox+text row has
       nothing else pinned to the right edge to worry about, but a photo-carrying one
       does. */
    .touch-mode .task-row {
      padding: 18px 16px 18px 16px;
    }
    /* Section-app rows' own trailing content (dots/badges/buttons/value), right-edge
       gap unified to an ordinary task's own baseline on desktop (2026-09-11 follow-up) —
       Malte: "the section apps and ordinary tasks have different padding to the right...
       The baseline should be the space an ordinary task with a photo has to the right
       edge." That thumbnail is `position:absolute; right:8px`, measured against
       `.task-row`'s own padding edge regardless of the row's own padding-right — 8px on
       desktop, confirmed live (getBoundingClientRect) rather than assumed. These five
       kinds' own trailing content is a normal flex child instead, so on desktop it just
       sits wherever the row's own 16px padding-right happens to land it (8px over
       baseline) without this. */
    .metric-task .task-row, .medication-task .task-row, .habit-task .task-row,
    .potty-log-task .task-row, .gemjar-task .task-row, .bp-task .task-row {
      padding-right: 8px;
    }
    /* Touch mode needs its own explicit override after all — a real regression caught
       live (2026-09-11, same day): removing this the first time, on the assumption that
       the shared `.touch-mode .task-row` rule above would "just" supply the same 16px
       for free once it also read 16px, missed that this rule and that one have *equal*
       specificity (two classes each) — a tie which source order alone resolves, and this
       one sits later in the file, so it silently kept winning in touch mode too and
       these five kinds quietly stayed at 8px there. Malte: "did you remove it from the
       section apps on the right again? Both them and the ordinary task carrying an image
       should have it." Fixed properly this time with an actual specificity edge (three
       classes vs two), not just hoping cascade order stays lucky. */
    .touch-mode .metric-task .task-row, .touch-mode .medication-task .task-row,
    .touch-mode .habit-task .task-row, .touch-mode .potty-log-task .task-row,
    .touch-mode .gemjar-task .task-row, .touch-mode .bp-task .task-row {
      padding-right: 16px;
    }
    /* Bigger checkbox */
    .touch-mode .task-check {
      width: 24px; height: 24px; margin-top: 0px; flex-shrink: 0;
    }
    /* Task dialog section icons, touch sizing (2026-09-11) — Malte: "the icons need to
       be a bit bigger to match the checkbox. Also the text for everything but the title
       ... needs to nudge a bit to the right, so everything is in line." One fix covers
       both: the icon column's own 20px width (i.task-detail-row-icon's base rule) never
       grew to match this touch checkbox's 24px, so every row *after* an icon (note,
       due-date, "+ Media") sat 4px left of the title row's own content — measured live
       (getBoundingClientRect) rather than assumed, confirmed exactly 4px, exactly the
       20px-vs-24px difference. Widening the icon column to the same 24px removes that
       gap directly, without a separate margin/nudge rule needed — .task-detail-icon-row's
       shared 10px gap then lines both rows' content up on its own. font-size scaled by
       the same ~1.2x every other touch target here gets (checkbox 20->24).
       Line-height set to a real 24px too, but the actual visible glyph is still
       measurably smaller than that box (a real rendering-vs-font-size gap this weight
       has, see i.task-detail-row-icon's own comment) — no attempt made to force the
       glyph itself to fill 24px exactly, only to match the same *proportion* the desktop
       size already reads correctly at. */
    .touch-mode i.task-detail-row-icon { width: 24px; font-size: 1.8rem; line-height: 24px; }
    /* Task overflow icon, touch sizing (2026-09-07) — the base .task-row-overflow rule
       only ever matched the header's own "More options" icon (.icon-btn-top) at its
       @media (pointer:fine) desktop size (1rem/16px); .icon-btn-top's own *base* size —
       what it actually is before that media query shrinks it for a mouse — is 1.25rem
       (20px), which is what a touch device actually sees there. Measured live to
       confirm rather than assumed. 34px box, not 28 — same ~1.2x bump every other touch
       target here gets (checkbox 20->24), and happens to land on the same 34px
       .icon-btn-top itself uses at its own desktop size, a coincidence worth keeping
       rather than picking a fresh unrelated number. top:13px centers it on the
       touch-mode checkbox's own vertical center (measured live: row padding-top 18px,
       checkbox height 24px, no margin-top in touch mode, box height 34px). */
    .touch-mode .task-row-overflow {
      top: 13px; width: 34px; height: 34px;
    }
    .touch-mode .task-row-overflow i { font-size: 1.25rem; }
    .touch-mode .task-check:checked::after {
      left: 6px; top: 2px; width: 8px; height: 13px;
    }
    /* font-size only, no width override (2026-09-12 follow-up) — the base rule's own
       `width: 1em` already tracks whatever font-size is active, touch mode included; see
       its own comment for why a fixed px width (was 24px, and 1.4rem here — a further,
       independent small mismatch against i.task-app-icon's own 1.44rem below) doesn't
       keep the alignment this needs. */
    .touch-mode .gemjar-jar { font-size: 1.44rem; }
    .touch-mode .gemjar-count { font-size: .58rem; }
    /* metric/medication/habit/potty-log row icon, touch sizing (2026-09-11 follow-up) —
       same ~1.2x bump every other touch target here gets (checkbox 20->24, Gem Jar's own
       icon just above), applied to the one section-app icon shape that hadn't gotten it
       yet. 1.44rem = 1.2rem (the shared desktop size, see i.task-app-icon's own comment)
       * 1.2. Measured live afterward, not just trusted from the ratio: on a row that
       still uses the default flex-start alignment (medication/habit/potty-log — see
       .task-check's own margin-top for the checkbox's half of this comparison), this
       icon's vertical center now lands within half a pixel of the checkbox's own offset
       from the (also touch-bumped) task text next to it. The metric tracker's own row
       uses align-items:center instead (.metric-task .task-row), which self-centers each
       item regardless of its height — no separate margin-top needed there either. */
    .touch-mode i.task-app-icon { font-size: 1.44rem; }
    /* showDateOrRepeatPicker's own "Repeat" checkbox (2026-09-10 follow-up) — Malte: "can
       you make the repeat checkbox bigger on mobile? It is still quite small." Same
       ~1.2x touch-mode bump .task-check itself gets, and the label row gets a bit more
       vertical padding too so the whole row (not just the box) is a comfortable tap
       target, matching .task-check's own row (padding-top bumped 0->18px in touch mode). */
    .repeat-picker-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; cursor: pointer; font-size: .82rem; color: var(--text); font-weight: 500; }
    .repeat-picker-chk { accent-color: var(--accent); width: 16px; height: 16px; cursor: pointer; }
    .touch-mode .repeat-picker-row { padding: 6px 0; font-size: .92rem; }
    .touch-mode .repeat-picker-chk { width: 22px; height: 22px; }
    /* Task text larger — every section-app's own name class joins this same bump now
       (2026-09-11 follow-up, same pass as unifying their desktop size above) — medication/
       habit/potty-log had no touch-mode override of their own at all, so while every
       other kind of row's text grew on touch, theirs silently stayed desktop-sized,
       reopening the exact "doesn't match ordinary tasks" gap this whole pass was fixing,
       just in the other pointer context. */
    .touch-mode .task-text,
    .touch-mode .metric-task-name,
    .touch-mode .medication-task-name,
    .touch-mode .habit-task-name,
    .touch-mode .potty-log-task-name,
    .touch-mode .bp-task-name { font-size: 1.05rem; line-height: 1.55; }
    /* Bigger note excerpt rows */
    .touch-mode .task-note-excerpt { padding: 6px 16px 10px; font-size: .95rem; }
    .touch-mode .task-overview-subtitle { padding: 0 16px 10px; margin-top: -2px; }
    /* Larger overflow menus in touch mode */
    .touch-mode .menu-drop { min-width: 220px; }
    .touch-mode .menu-item { padding: 14px 18px; font-size: .95rem; gap: 12px; }
    .touch-mode .menu-item i { font-size: 1.1rem; }
    /* Hide section drag handles — section reordering stays desktop-only for now */
    .touch-mode .sec-head .handle { display: none !important; }
    /* Task drag handles: bigger, always-visible, in-flow touch target (see bindTouchTaskHandle in app.js) */
    .touch-mode .task-row .handle {
      display: flex !important; position: static; opacity: 1;
      align-items: center; justify-content: center;
      align-self: center; /* .task-row is align-items:flex-start (so multi-line notes
        don't drag the checkbox down with them) — this re-centers just the handle
        against the checkbox/text instead of sitting flush at the row's top edge */
      width: 36px; height: 36px; margin: -8px -4px -8px -10px;
      font-size: 1.15rem; color: var(--muted);
    }
    .task.touch-dragging { opacity: .5; box-shadow: 0 6px 16px rgba(0,0,0,.18); position: relative; z-index: 50; }
    .task.touch-drop-above { box-shadow: inset 0 3px 0 var(--accent); }
    .task.touch-drop-below { box-shadow: inset 0 -3px 0 var(--accent); }
    /* Taller section headers */
    .touch-mode .sec-head { min-height: 52px; }
    /* .sec-more-btn's own size (2026-08-29) now always matches .icon-btn-top exactly —
       48px by default, matching the app header's own touch sizing — so no separate
       touch-mode override is needed here anymore. */
    /* Chip row (date/repeat/photo-add) — bigger tap targets */
    .touch-mode .task-tag-add,
    .touch-mode .task-tag,
    .touch-mode .task-due,
    .touch-mode .task-recurrence-pill { min-height: 36px; padding: 6px 12px; font-size: .95rem; }
    /* Note area tap target */
    .touch-mode .task-note-area { padding-bottom: 12px; }
    /* Rendered note text matches the editing textarea's own size on mobile (2026-09-11)
       — Malte: "increase the rendered note font-size to be the same when editing."
       Both already share the identical .note-view class/rule (.84rem/13.44px) with no
       prior touch-mode override — confirmed directly (getComputedStyle) rather than
       assumed, on both the rendered view and the actual textarea, before writing this
       rule, since there was nothing here to explain a difference in the CSS itself. The
       real-device mismatch is almost certainly mobile Safari/Chrome's own well-known
       auto-zoom-on-focus for any input/textarea under 16px — the browser zooms the whole
       viewport in while editing to keep sub-16px text legible, then back out on blur,
       which reads exactly like "editing looks bigger" without the two ever actually
       differing in CSS. Bumping the shared rule to a flat 16px for both at once — past
       that threshold, the browser doesn't auto-zoom at all, so this removes the zoom
       jump itself, not just the two sizes' own gap. */
    .touch-mode .note-view { font-size: 16px; }
    /* Icon vertically centered against its own row's pill, not the plain-text margin-top
       tuned for a line of running text (2026-09-11, retuned same day) — Malte: "align
       the icon horizontally with the text-button then." Originally scoped to the due/
       media *rows*, back when "+ Schedule"/"+ Media" were always small pill buttons
       there — measured live (getBoundingClientRect) that a 36px-tall pill/button needs
       6px here where a line of plain text only needs 2px. Once the empty ("Add a
       schedule…"/"Add media…") states became plain .note-view text rows matching notes'
       own placeholder, and the media row started showing the photo banner directly
       instead, "is this row due/media" stopped being the same question as "is there
       actually a pill in it" — retargeted to the new .task-detail-icon-row-pill modifier
       class (set only when a real recurrence/due-date pill is showing) so the empty
       state and the photo banner both correctly fall back to the plain 2px default
       instead of inheriting a nudge tuned for a button that's no longer there. */
    .touch-mode .task-detail-icon-row-pill .task-detail-row-icon { margin-top: 6px; }
