/* Shared shell chrome: gate, toolbar/nav, and the generic list/item
   primitives every module reuses (band/wrap/item/section-heading/btn).
   Module-specific rules belong in that module's own module.css, nested
   under .module-<id> — this file is the shared surface only. */

/* ================= gate (signed out) ================= */
#gate{
  position:fixed; inset:0; overflow:hidden;
  background:var(--gate-bg);
  display:flex; align-items:center; justify-content:center;
}
/* Programmatically focused (see index.html's tabindex="-1" comment and
   auth.js) purely to satisfy WebAuthn's document-focus requirement — no
   visible ring wanted for that. */
#gate:focus{ outline:none; }
#gate .gate-vignette{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  opacity:.09; pointer-events:none;
  filter:grayscale(1);
}
/* An uploaded photo is meant to actually be seen, not hidden as a faint
   watermark — full-bleed cover, higher opacity, no forced grayscale. */
#gate .gate-vignette.has-custom{ opacity:.55; filter:none; }
/* Reverted back to cover (tried contain, letterboxing the whole emblem
   on every viewport shape -- explicitly not wanted, reads as too small/
   floating; the tighter, cropped-in cover look was preferred). */
#gate .gate-vignette.has-custom img{ width:100%; height:100%; object-fit:cover; }
#gate::after{
  /* soft radial vignette, darkens the edges further */
  content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(ellipse at center, rgba(0,0,0,0) 40%, rgba(0,0,0,.55) 100%);
}
/* Only appears while WebAuthn is actively waiting for the user's passkey.
   Layered inset glows keep the effect at the viewport edge, where it reads
   as an energized red plasma rim without obscuring the sign-in controls. */
#gate.auth-pending::before{
  content:""; position:absolute; inset:0; z-index:3; pointer-events:none;
  box-shadow:
    inset 0 0 4px 2px rgba(255,238,230,.95),
    inset 0 0 14px 5px rgba(255,49,35,.92),
    inset 0 0 34px 10px rgba(176,0,20,.72),
    inset 0 0 68px 18px rgba(88,0,16,.38);
  animation:gate-plasma-pulse 1.15s ease-in-out infinite alternate;
}
@keyframes gate-plasma-pulse{
  from{
    opacity:.72;
    filter:saturate(.9) brightness(.88);
  }
  to{
    opacity:1;
    filter:saturate(1.35) brightness(1.18);
  }
}
@media (prefers-reduced-motion:reduce){
  #gate.auth-pending::before{ animation:none; }
}
#gate .gate-content{
  position:relative; z-index:1;
  width:100%; max-width:360px; padding:0 24px; text-align:center;
  animation:gate-fade-in .6s ease-out;
}
@keyframes gate-fade-in{ from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:none;} }
#gate .gate-eyebrow{
  /* Deliberately not --font-headline (Fraunces/Iowan) here — this is the
     one piece of type on the gate, and a plain formal serif reads more
     like an institutional sign-in than a display face does. Bold rather
     than a mid weight: none of this stack ships real intermediate weights,
     so anything between 400 and 700 just gets rounded to one of those two
     by the browser anyway. */
  font-family:Georgia,'Times New Roman','Iowan Old Style',serif; font-weight:700; font-size:15px;
  letter-spacing:.28em; text-transform:uppercase; color:var(--gate-text);
  margin:0 0 40px;
}
/* Empty until gate-title.js applies the saved value — the HTML ships with
   no text so there's nothing hardcoded to flash before that load finishes. */
#gate .gate-eyebrow:empty{ display:none; }
#gate .gate-btn-primary{
  /* 90% transparent background (a "ghost" button) rather than fading the
     whole element including its label — full-opacity text keeps it
     legible against the dark gate background showing through. Sized down
     ~40% from the original full-width button: width, padding and font all
     scaled together rather than just squeezed narrower. */
  display:block; width:60%; margin:0 auto; background:rgba(140,106,74,.1); color:var(--gate-text);
  font-weight:500; font-size:11px; letter-spacing:.02em; font-family:inherit;
  border:1px solid rgba(140,106,74,.45); border-radius:6px; padding:8px 12px; cursor:pointer;
}
#gate .gate-btn-primary:hover{ background:rgba(140,106,74,.2); border-color:var(--gate-accent); }
#gate .gate-msg{ font-size:12.5px; margin-top:16px; color:var(--gate-text-soft); min-height:16px; }
#gate .gate-recovery{
  margin:28px auto 0; padding-top:22px; max-width:300px;
  border-top:1px solid rgba(140,106,74,.35); text-align:left;
}
#gate .gate-recovery[hidden]{ display:none; }
#gate .gate-recovery label{
  display:block; margin-bottom:8px; color:var(--gate-text); font-size:12px;
}
#gate .gate-recovery input,
#gate .gate-recovery button{
  width:100%; border-radius:6px; font:inherit;
}
#gate .gate-recovery input{
  padding:9px 10px; color:var(--gate-text); background:rgba(10,10,12,.78);
  border:1px solid rgba(140,106,74,.55);
}
#gate .gate-recovery button{
  margin-top:9px; padding:9px 10px; cursor:pointer; color:var(--gate-text);
  background:rgba(140,106,74,.18); border:1px solid var(--gate-accent);
}
#gate .gate-recovery .gate-msg{ text-align:center; }
/* Pinned to the very top of the gate, not just the top of the vertically
   centered .gate-content block below it. */
#gate .gate-network-status{
  position:absolute; top:0; left:0; right:0; z-index:1;
  padding:16px 24px 0; text-align:center;
  font-size:11px; letter-spacing:.01em; color:var(--gate-text-soft);
  word-break:break-word;
}
#gate .gate-network-status:empty{ display:none; }

/* ================= app shell (signed in) ================= */
#app{display:none;}

/* ---- top bar: menu + settings (left), driftstatus (center) -- sticky
   so it, and the icons on it, stay reachable while scrolling a long
   module, without needing a second fixed bar at the bottom. Network
   status (IPv4/IPv6/VPN) used to live here too as a third right-hand
   column, but kept visually colliding with the driftstatus strip next to
   it (worst on Command, where that strip is widest) -- moved out to its
   own thin footer instead, see .app-footer further down. ---- */
/* Grid, not flex, for the two top-bar zones -- a real device (iOS
   Safari) rendered a flex:1 1 auto middle zone as if it were only ever as
   wide as its own (wrapped, centered) content, instead of growing to
   fill the leftover row space the way it does in every Chromium-based
   browser used to test this locally. `auto 1fr` sidesteps the ambiguity
   entirely: the first column is always exactly its own content's width,
   the second is a plain, unambiguous "eat whatever's left" track that
   centers driftstatus within it (see .top-bar-service-status's own
   justify-self) -- no flex-basis-vs-wrapped-content judgment call left
   for any browser to get wrong. */
/* Nav (Brief/Sarek/Portfolio/Log) now lives in a fixed right-edge tab
   stack (see .edge-tab-stack below), and settings/sign-out move to a
   fixed left-edge stack on desktop -- the top bar itself is down to just
   the hamburger (mobile only) and driftstatus. */
.top-bar{
  position:sticky; top:0; z-index:15;
  display:grid; grid-template-columns:auto 1fr; align-items:center; gap:16px;
  padding:12px 20px;
  background:var(--bg); border-bottom:1px solid var(--hairline);
}
.top-bar-group{ grid-column:1; grid-row:1; display:flex; align-items:center; gap:8px; }
.top-bar-service-status{ grid-column:2; grid-row:1; min-width:0; justify-self:center; }
/* Driftstatus, expanded, wraps its own content onto multiple lines
   (see command-center/module.css's body.status-panel-open rules) and
   grows taller than a single icon row -- switching to align-items:start
   keeps the menu/settings/sign-out icons pinned to the top of that now-
   tall row instead of drifting to its vertical center. */
body.status-panel-open .top-bar{
  position:relative;
  align-items:start;
}
.icon-btn{
  display:inline-flex; align-items:center; justify-content:center;
  width:34px; height:34px; padding:0;
  background:none; border:1px solid var(--hairline); border-radius:8px;
  color:var(--ink-soft); cursor:pointer;
}
.icon-btn:hover{ border-color:var(--ink-soft); color:var(--ink); }
.icon-btn .edge-tab-label{ display:none; }

/* ---- footer: IPv4/IPv6/VPN only (2026-07-31) -- used to be a third
   column crammed into .top-bar alongside driftstatus, which on Command
   (the widest driftstatus strip) kept visually colliding with it, worst
   on narrower screens. A thin bar of its own, pinned to the bottom
   instead of the top, has the full width to itself and nothing left to
   collide with. Fixed + sticky-equivalent (position:fixed rather than
   sticky, since it needs to stay put even when the page is shorter than
   the viewport, not just while scrolling) -- #module-content gets
   matching bottom padding below so the last bit of a module's content
   never sits underneath it. */
.app-footer{
  position:fixed; left:0; right:0; bottom:0; z-index:15;
  display:flex; align-items:center; justify-content:center;
  min-height:30px; padding:6px 20px;
  background:var(--bg); border-top:1px solid var(--hairline);
}
/* Nothing to show (both IP lookups failed) collapses the footer down to
   nothing rather than leaving an empty hairline-bordered strip -- :has()
   support is safe to rely on here (Safari 15.4+/Chrome 105+, both long
   since current by this app's one-user, evergreen-browser reality). */
.app-footer:has(> .network-status:empty){ display:none; }
#module-content{ padding-bottom:42px; }

.session-timer{
  font-size:11px; letter-spacing:.01em; color:var(--ink-grey); white-space:nowrap;
}
.session-timer:empty{ display:none; }
.session-timer.session-timer-warn{ color:var(--status-warn); font-weight:600; }

.network-status{
  min-width:0;
  font-size:11px; letter-spacing:.01em; color:var(--ink-grey); text-align:center;
  word-break:normal;
}
.network-status:empty{ display:none; }

/* IPv4 / IPv6 / VPN, run inline in one line -- back to a plain row
   (2026-07-31) now that this lives in its own dedicated footer strip
   with the full width to itself: stacking these vertically was a
   workaround for the top-bar collision, not the goal in itself, and a
   vertical stack would just make a "thin footer" tall instead. Colors
   inherit via currentColor so this reads correctly both here (light, on
   --ink-grey) and on the dark gate (see #gate .gate-network-status
   below, on --gate-text-soft). */
.net-pill{
  display:inline-flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:4px 14px; padding:4px 0; opacity:.82;
}
.net-item{ display:inline-flex; align-items:center; gap:5px; white-space:nowrap; }
.net-badge{ font-size:9px; font-weight:700; letter-spacing:.04em; opacity:.75; }
.net-shield{ flex:0 0 auto; }
.net-item.net-vpn{ color:#2f9e6e; }

.nav{ display:flex; flex-wrap:wrap; gap:8px; }
.nav button{
  position:relative;
  background:none;border:1px solid var(--hairline);border-radius:8px;padding:6px 14px;
  font-family:inherit;font-size:13px;color:var(--ink-soft);cursor:pointer;
  transition:border-color .12s ease, color .12s ease, background .12s ease;
}
/* Hover feedback was missing entirely -- every other clickable control
   in the top bar (.icon-btn, .btn) already gives some, so an inactive
   nav button was the one thing in the row that looked clickable only by
   cursor:pointer, with nothing confirming it under the mouse. */
.nav button:not(.active):hover{ border-color:var(--ink-soft); color:var(--ink); }
/* --clay (the app's one warm accent, used everywhere else a primary
   action needs to stand out -- see .btn) rather than flat --ink/black,
   so the active tab reads as "the same accent as the rest of the app"
   instead of an unrelated dark pill. */
.nav button.active{ background:var(--clay); color:var(--bg); border-color:var(--clay); }
/* Unseen-activity dot (see module-registry.js's getBadgeCount) -- a
   count would need real estate this small a button doesn't have; a dot
   only ever means "something changed since you last looked here." */
.nav-badge{
  position:absolute; top:4px; right:4px; width:7px; height:7px; border-radius:50%;
  background:var(--status-warn); border:1.5px solid var(--bg);
}
.nav button.active .nav-badge{ border-color:var(--clay); }

/* ---- Edge tabs (2026-07-25; mobile/desktop split removed 2026-07-31):
   navigation (Brief/Sarek/Portfolio, built by module-registry.js's
   buildNav) plus Log (a bespoke drawer, see shell/log-drawer.js) sit as a
   stack of small vertical tabs pinned to the right edge of the viewport,
   instead of inline buttons in the top bar -- on every screen size now,
   not just wide ones. Settings/sign-out get the mirrored treatment on the
   left, desktop-only (pure CSS repositioning, no reparenting needed --
   see further down); on a phone they stay plain top-bar icons instead,
   since unlike the nav tabs they were never hidden behind a drawer that
   could go unreachable, and two permanent edge stacks plus a top bar is
   tight on a narrow screen. (ToDo was part of this same right-edge stack
   too until 2026-07-25, when it moved back inside the Morning Brief
   module as a collapsible card instead.)

   Nav used to live in a hamburger-triggered #side-nav drawer below 860px
   instead of here -- removed 2026-07-31. That drawer's one open/close
   trigger sat in .top-bar-group, a grid cell that grows tall/reflows
   whenever #top-bar-service-status (its sibling cell) expands on a narrow
   screen, which could push the only way to reach the nav out of reach.
   Living here instead, position:fixed and outside that grid entirely,
   nav can't be affected by anything else happening in the top bar. */
.edge-tab-stack{
  position:fixed; top:50%; transform:translateY(-50%); z-index:16;
  display:flex; flex-direction:column; gap:8px;
}
.edge-tab-stack.right{ right:0; }
.edge-tab-stack.left{ left:0; }
.edge-tab-stack .nav{ display:contents; } /* the row/column flex on .nav doesn't matter here -- buttons join the stack's own flex column directly */

#nav-edge-tabs .nav button,
.edge-tab{
  cursor:pointer; user-select:none; border:none; font-family:inherit;
  font-size:13.5px; font-weight:600; letter-spacing:.03em; color:#fff;
  padding:18px 11px; min-height:84px; writing-mode:vertical-rl;
  box-shadow:0 1px 2px rgba(16,24,40,.06), 0 6px 16px rgba(16,24,40,.12);
  transition:box-shadow .15s ease, transform .15s ease, filter .15s ease;
}
/* #nav-edge-tabs is always the *right* stack (hardcoded "right" in
   index.html, there's only ever one) -- no need to qualify it with
   .edge-tab-stack.right, that combination can only ever describe a
   *different* .edge-tab living inside it (like the Log tab), never
   #nav-edge-tabs itself. An earlier version got this backwards (chained
   .edge-tab-stack.right as an ancestor of #nav-edge-tabs, which can't
   ever match since they're the same element) and silently no-opped
   every rule below -- the border-radius/hover-nudge quietly fell back
   to shell.css's much older, unrelated 8px .nav button default instead. */
#nav-edge-tabs .nav button{ border-radius:14px 0 0 14px; }
.edge-tab-stack.right .edge-tab{ border-radius:14px 0 0 14px; }
/* border-radius is painted in the box's own *un-rotated* frame, then
   transform:rotate(180deg) flips the whole rendered result -- so to end
   up with the rounded side visually facing the page content (and the
   flat side against the screen edge) after that flip, the radius has to
   be defined backwards from what you'd expect for an unrotated left-edge
   box (round-then-flip = round on the *opposite* physical side you
   drew it on). Wrong the first time (0 14px 14px 0), which put the flat
   side facing the content and the round side jammed into the edge --
   exactly backwards, and exactly what Dario flagged. */
.edge-tab-stack.left .edge-tab{ transform:rotate(180deg); border-radius:14px 0 0 14px; }
/* A small nudge away from the edge plus a brighter fill on hover -- the
   flat, static blocks felt inert/"iffy" (Dario's word) with no feedback
   at all under the mouse, unlike every other clickable surface in the
   app (.icon-btn, .nav button, .btn all already give some). */
#nav-edge-tabs .nav button:hover,
.edge-tab:hover{ filter:brightness(1.12); box-shadow:0 2px 6px rgba(16,24,40,.1), 0 10px 22px rgba(16,24,40,.16); }
#nav-edge-tabs .nav button:hover{ transform:translateX(-3px); }
.edge-tab-stack.right .edge-tab:hover{ transform:translateX(-3px); }
.edge-tab-stack.left .edge-tab:hover{ transform:rotate(180deg) translateX(-3px); }
#nav-edge-tabs .nav button:focus-visible,
.edge-tab:focus-visible{ outline:2px solid #fff; outline-offset:-4px; }
/* Page-navigation tabs (Brief/Sarek/Portfolio) vs the fold-out-drawer
   tabs (Log, Settings) get visibly different colors -- clicking one
   replaces the whole page, the other slides a panel over it, and that
   distinction is exactly what Dario asked to see at a glance. Sign-out
   isn't really either (it's a confirmation prompt, not navigation or a
   panel), so it just stays neutral alongside the page tabs. */
#nav-edge-tabs .nav button{ background:var(--ink); }
#nav-edge-tabs .nav button.active{ background:var(--clay); }
#nav-edge-tabs .nav-badge{ top:4px; right:4px; transform:none; }
.edge-tab.edge-tab-page{ background:var(--ink); }
.edge-tab.edge-tab-drawer{ background:#2F6FE4; }

@media (min-width:860px){
  /* Settings/sign-out stay physically inside .top-bar-group in the DOM
     (so mobile keeps them exactly where they've always been -- see
     auth.js/shell.js, nothing about their wiring changes) but escape its
     layout entirely here, becoming their own small left-edge tab stack. */
  #open-settings-btn{
    display:inline-block; position:fixed; left:0; z-index:16;
    /* Same backwards-from-usual radius as .edge-tab-stack.left .edge-tab
       above, for the same reason: rotate(180deg) flips which physical
       side the rounded corners land on. */
    width:auto; height:auto; min-height:84px; padding:18px 11px; border:none; border-radius:14px 0 0 14px;
    writing-mode:vertical-rl; transform:rotate(180deg); /* pure visual flip -- top below positions the box itself, unaffected by this */
    font-size:13.5px; font-weight:600; letter-spacing:.03em; color:#fff;
    box-shadow:0 1px 2px rgba(16,24,40,.06), 0 6px 16px rgba(16,24,40,.12);
    transition:box-shadow .15s ease, filter .15s ease;
  }
  #open-settings-btn:hover{
    filter:brightness(1.12); box-shadow:0 2px 6px rgba(16,24,40,.1), 0 10px 22px rgba(16,24,40,.16);
    transform:rotate(180deg) translateX(-3px); /* real screen-space +3px (away from the left edge) -- rotate(180deg) flips a local translateX's sign */
  }
  #open-settings-btn:focus-visible{ outline:2px solid #fff; outline-offset:-4px; }
  #open-settings-btn svg{ display:none; } /* icon only makes sense un-rotated in the edge tab */
  #open-settings-btn .edge-tab-label{ display:inline; }
  /* Settings opens a drawer (drawer-blue, same as Log); sign-out
     triggers a confirm prompt, not a panel, so it stays neutral. `top`
     (a layout property, resolved before `transform`) positions each box
     directly -- rotate() above only spins each one in place around its
     own center afterward, so it can't shift these positions around the
     way mixing rotate() with translateY() in the same transform did (the
     first version had settings and sign-out swapped top-to-bottom for
     exactly that reason). */
  #open-settings-btn{ top:calc(50% - 100px); background:#2F6FE4; }
  #signout-btn{ width:auto; padding:0 12px; gap:7px; color:var(--ink); background:rgba(255,255,255,.55); }
  #signout-btn .edge-tab-label{ display:inline; }
}

.btn{
  display:inline-block; background:var(--clay); color:var(--bg); font-weight:500; font-size:13.5px;
  border:1px solid var(--clay); border-radius:8px; padding:10px 18px; cursor:pointer; text-decoration:none;
  font-family:inherit;
}
.btn:hover{ background:var(--clay-hover); border-color:var(--clay-hover); }

.band{width:100%;}
.band-top{background:var(--wash);}
.wrap{max-width:860px;margin:0 auto;padding:28px 28px 36px;}

/* Fixed, full-viewport layer behind everything -- see
   shell/dashboard-background.js for why this is a real element rather
   than a background-attachment:fixed style on <body> (the latter
   doesn't reliably cover a page taller than one screen, especially on
   iOS Safari). z-index:-1 keeps it behind #app without needing #app
   itself to be positioned. */
#dashboard-bg-layer{
  position:fixed; inset:0; z-index:-1;
  background-size:cover; background-position:center; background-repeat:no-repeat;
}
/* html,body's own background (tokens.css) becomes the *canvas*
   background when html has none set, which paints through/underneath
   z-index:-1 content in a way that made #dashboard-bg-layer invisible in
   testing despite correct stacking on paper -- make body transparent
   itself whenever the layer is meant to be seen, so there's nothing
   left to compete with it. */
html.has-custom-bg, body.has-custom-bg{ background:transparent; }

/* A custom dashboard background is otherwise only ever visible behind
   the toolbar strip, since every surface below it is fully opaque --
   make it translucent instead, whenever a custom image is actually
   active, so the photo still reads through as a frame around the real
   content instead of being fully hidden.

   No backdrop-filter/blur anywhere in this whole has-custom-bg block,
   on purpose -- confirmed on a real iPhone that iOS Safari renders
   *multiple simultaneous* backdrop-filter layers on one screen
   incorrectly: with the top bar, the brief card, and the todo card all
   blurring at once, some of them clipped their blurred backing to
   roughly 70% of their own width, showing the raw un-blurred photo past
   that point. Two rounds of trying to keep the blur (moving it to a
   ::before, guessing it was a position:sticky-specific bug) didn't fix
   it, because the real trigger was simply "more than one blurred layer
   on screen at once" -- a single element's own backdrop-filter (like
   the pre-existing .band-top, back when it was the only one) never
   showed this. Plain alpha-blended color has no such compositing bug on
   any browser, at the cost of the photo reading a bit less "frosted"
   underneath each surface. */
body.has-custom-bg .band-top{
  width:auto;
  background:rgba(249,249,247,.97);
  margin:20px; border-radius:22px;
  box-shadow:0 16px 48px rgba(15,14,10,.18);
}
/* --ink-soft (#6B6A63) was tuned for a fully opaque --wash background
   (5.15:1 contrast there) and measured as low as ~1.9:1 once blended
   through a translucent card over a real photo -- nowhere near
   readable, and no alpha/blur combination on the card fixes that for
   every possible photo without going nearly opaque and defeating the
   point of a translucent card at all. Cheaper fix: give secondary text
   its own darker shade only in this mode, measured at ≥4.5:1 against
   the .97-alpha blend above (see the module's own contrast check).

   .97 rather than the .7-.85 tried earlier: dropping backdrop-filter
   (see the note above this block) means a busy, detailed photo like
   Dario's crest/map background no longer gets smoothed into a soft wash
   by the blur -- at .85 alpha its sharp edges (the crest's crown, in
   particular) still read as a distracting watermark right through the
   card's own text. .97 leaves just enough of the photo's tone bleeding
   through to still feel like a frame around the card, without the
   legible pattern-through-text problem. */
body.has-custom-bg .item-sentence,
body.has-custom-bg .act-note,
body.has-custom-bg .day-date,
body.has-custom-bg .quiet-line{ color:#4A473F; }
body.has-custom-bg .top-bar{ background:rgba(232,230,224,.94); border-bottom-color:#D2CFC7; }
body.has-custom-bg .app-footer{ background:rgba(232,230,224,.94); border-top-color:#D2CFC7; }
body.has-custom-bg .session-timer,
body.has-custom-bg .network-status,
body.has-custom-bg .topbar-clock{ color:#5F5C55; }
body.has-custom-bg .session-timer,
body.has-custom-bg .network-status{ color:#68655E; }
body.has-custom-bg .session-timer.session-timer-warn{ color:#ffb454; }

@media (max-width:640px){
  body.has-custom-bg .band-top{ margin:12px; border-radius:18px; }
  /* A title's usual content (event label + a full date/time stamp, for
     the access log) is more prone to overflowing a phone-width card
     than Brief's short hand-authored titles -- a smaller size buys back
     room before the ellipsis rule above has to kick in. Tighter gap/
     padding too: less dead space between rows, and the number/chevron
     column eats less of a narrow screen's width before the text starts. */
  .item{ gap:7px; padding:7px 0; }
  .item-title{ font-size:14px; gap:5px; }
  .item-num{ font-size:12px; min-width:11px; }
}

h2.section-heading{
  font-family:var(--font-body); font-weight:600; font-size:13.5px; letter-spacing:.02em;
  color:var(--ink); margin:24px 0 4px;
}

.item{ display:flex; gap:10px; padding:8px 0; border-top:1px solid var(--hairline); }
.item:first-of-type{border-top:none;}
.item-num{ font-size:13px; color:var(--ink-grey); min-width:13px; padding-top:2px; }
.item-body{flex:1; min-width:0;}

.item-title{
  display:flex; align-items:center; gap:6px;
  font-weight:600; font-size:15px; color:var(--ink); margin:-4px 0; padding:4px 0;
  cursor:pointer; user-select:none; border-radius:6px;
}
.item-title::before{
  content:"›"; flex:0 0 auto; display:inline-block;
  font-size:15px; line-height:1; color:var(--ink-grey);
  transition:transform .15s ease, color .15s ease;
}
.item-title:hover::before{ color:var(--clay); }
.item.expanded .item-title::before{ transform:rotate(90deg); }
.item-title:focus-visible{ outline:2px solid var(--clay); outline-offset:3px; }
/* Truncate rather than wrap -- a title is meant to read as one line with
   a chevron at the front; wrapping breaks that rhythm and, worse, can
   clip the far end of a title/timestamp against the card edge on a
   narrow phone instead of just shortening it with an ellipsis. */
.item-title-text{
  flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.item-link{
  flex:0 0 auto; font-size:12px; color:var(--ink-soft); text-decoration:none;
  padding:2px 4px; border-radius:4px;
}
.item-link:hover{ color:var(--clay); }

/* Small severity/status pill next to a title -- see dom-utils.js's
   renderItem. Generic grey by default; a module adds its own color
   variants via the second item-badge-<variant> class rather than this
   shared rule hard-coding any specific palette. */
.item-badge{
  flex:0 0 auto; font-size:10px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  padding:2px 8px; border-radius:999px; background:var(--hairline); color:var(--ink-soft);
}

/* Grid-rows accordion trick: animating 0fr → 1fr (rather than height:auto,
   which can't be transitioned) lets the collapsed/expanded swap slide
   instead of snap. The inner wrapper needs its own overflow:hidden since
   a grid track's size, unlike a block's, doesn't itself clip content. */
.item-detail{
  display:grid; grid-template-rows:0fr; opacity:0;
  transition:grid-template-rows .2s ease, opacity .15s ease;
}
.item.expanded .item-detail{ grid-template-rows:1fr; opacity:1; }
.item-detail-inner{ overflow:hidden; }
.item-sentence{ font-size:14px; line-height:1.55; color:var(--ink-soft); padding-top:8px; }
.item .btn{ margin-top:10px; }

.empty-state{ max-width:860px;margin:0 auto;padding:48px 32px;text-align:center;color:var(--ink-soft);font-size:15px; }
.module-error{ max-width:860px;margin:0 auto;padding:40px 32px;color:var(--clay-hover);font-size:14px; }

/* ---- settings panel (appearance + sign-in + this device's encryption key) ---- */
/* Opens from the left now (2026-07-25) -- mirrors ToDo/Log opening from
   the right, and pairs with the settings tab itself moving to the left
   edge (see the #open-settings-btn rules above). */
.security-panel{
  position:fixed; top:0; left:0; bottom:0; width:360px; max-width:90vw;
  background:var(--bg); border-right:1px solid var(--hairline); padding:28px;
  transform:translateX(-100%); transition:transform .2s ease; z-index:20;
  overflow-y:auto;
}
.security-panel.open{ transform:none; }
.security-panel h2{ font-family:var(--font-headline); font-size:21px; margin:0 0 6px; color:var(--ink); }

/* Top-level grouping so the panel reads as a few named categories
   instead of one long undifferentiated scroll of h3 subheads -- the
   more settings get added over time, the more that flat list turns into
   noise. Native <details>/<summary> rather than a bespoke toggle: free
   keyboard/a11y support, and nothing in shell.js needs to know these
   groups exist -- every input keeps the same id it always had, just
   nested one level deeper, so all the existing getElementById wiring
   still finds everything unchanged. Chevron reuses the same "›"
   rotate-90-when-open language as .item-title (see the .item rules
   below) instead of inventing a second disclosure convention.*/
.security-panel .settings-category{ margin-top:0; }
.security-panel .settings-category > summary{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  font-family:var(--font-headline); font-weight:600; font-size:15.5px; color:var(--ink);
  padding:14px 0; border-top:1px solid var(--hairline); cursor:pointer; user-select:none;
  list-style:none;
}
.security-panel .settings-category:first-of-type > summary{ border-top:none; padding-top:4px; }
.security-panel .settings-category > summary::-webkit-details-marker{ display:none; }
.security-panel .settings-category > summary::after{
  content:"›"; flex:0 0 auto; font-size:16px; line-height:1; color:var(--ink-grey);
  transition:transform .15s ease;
}
.security-panel .settings-category[open] > summary::after{ transform:rotate(90deg); }
.security-panel .settings-category-body{ padding-bottom:6px; }

.security-panel .panel-subhead{
  font-family:var(--font-body); font-weight:600; font-size:14.5px; letter-spacing:.01em;
  color:var(--ink); margin:30px 0 8px;
}
.security-panel .panel-subhead:first-of-type{ margin-top:8px; }
.security-panel p{ font-size:13.5px; line-height:1.5; color:var(--ink-soft); margin:0 0 14px; }
.security-panel .panel-msg{ font-size:12.5px; color:var(--ink-soft); margin-top:10px; min-height:15px; }
.security-panel .toggle-row{
  display:flex; align-items:center; gap:9px; font-size:13.5px; color:var(--ink);
  cursor:pointer; user-select:none; margin:6px 0 12px;
}
.security-panel .toggle-row input{ width:15px; height:15px; accent-color:var(--clay); cursor:pointer; }
.security-panel .field-label{
  display:block; font-size:12.5px; font-weight:600; color:var(--ink); margin:16px 0 6px;
}
.security-panel .text-input{
  width:100%; padding:9px 12px; border:1px solid var(--hairline); border-radius:8px;
  font-size:13.5px; font-family:inherit; color:var(--ink); background:var(--wash);
}
.security-panel .text-input:focus{ outline:none; border-color:var(--clay); }
.security-panel select.text-input{ cursor:pointer; }
.security-panel .field-hint{ font-size:12px; color:var(--ink-soft); margin:6px 0 0; }
.security-panel .color-row{ display:flex; align-items:center; gap:12px; }
.security-panel .row-inputs{ display:flex; gap:12px; }
.security-panel .row-inputs > div{ flex:1; min-width:0; }
.security-panel input[type="color"]{
  width:44px; height:32px; padding:2px; border:1px solid var(--hairline); border-radius:6px;
  background:none; cursor:pointer;
}
.security-panel .device-list{ margin-top:6px; }
.security-panel .device-row{
  display:flex; justify-content:space-between; align-items:center; gap:12px; font-size:13.5px;
  padding:11px 0; border-top:1px solid var(--hairline);
}
.security-panel .remove-btn{
  font-size:12px; color:var(--clay-hover); background:none; border:none; cursor:pointer;
  text-decoration:underline; font-family:inherit; padding:0; white-space:nowrap;
}
.security-panel .close-btn{
  position:absolute; top:18px; right:18px; background:none; border:none; cursor:pointer;
  font-size:20px; color:var(--ink-grey); line-height:1;
}
.admin-invite-row{ display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; }
.admin-invite-row .btn{ white-space:nowrap; }
.admin-users-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:18px; }
.admin-users-head .panel-subhead{ margin:0; }
.admin-users-list{ display:grid; margin-top:8px; }
.admin-user-row{
  display:grid; grid-template-columns:minmax(0,1fr) 116px; gap:12px; align-items:center;
  padding:12px 0; border-top:1px solid var(--hairline);
}
.admin-user-row strong,.admin-user-row span{ display:block; overflow-wrap:anywhere; }
.admin-user-row strong{ font-size:13px; color:var(--ink); }
.admin-user-row span{ margin-top:4px; color:var(--ink-soft); font-size:11px; line-height:1.4; }
.security-panel .admin-role-select{ padding:7px 8px; font-size:12px; }

/* ---- Sign-out confirmation (see auth.js) -- a small centered dialog
   rather than the browser's native confirm(), so it matches the rest of
   the app's look instead of a jarring native popup. ---- */
.confirm-backdrop{
  position:fixed; inset:0; z-index:24; background:rgba(16,24,40,.35);
  opacity:0; pointer-events:none; transition:opacity .15s ease;
}
.confirm-backdrop.open{ opacity:1; pointer-events:auto; }
.confirm-dialog{
  position:fixed; top:50%; left:50%; z-index:25; width:320px; max-width:88vw;
  background:var(--bg); border-radius:14px; padding:22px; text-align:center;
  box-shadow:0 20px 60px rgba(16,24,40,.25);
  opacity:0; pointer-events:none; transform:translate(-50%,-50%) scale(.96);
  transition:opacity .15s ease, transform .15s ease;
}
.confirm-dialog.open{ opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1); }
.confirm-dialog p{ font-size:15px; color:var(--ink); margin:0 0 18px; }
.confirm-dialog-actions{ display:flex; gap:10px; justify-content:center; }
.confirm-dialog-cancel{
  background:none; border:1px solid var(--hairline); border-radius:8px; padding:10px 18px;
  font-family:inherit; font-size:13.5px; color:var(--ink-soft); cursor:pointer;
}
.confirm-dialog-cancel:hover{ border-color:var(--ink-soft); color:var(--ink); }

/* ---- Log slide-out drawer (see shell/log-drawer.js) -- global chrome,
   not tied to any one module's mount/unmount lifecycle, so it stays
   reachable from every page. Same right-edge open/close mechanics as
   #side-nav, just its own backdrop/z-index so the two never fight over
   the same one. (ToDo used to be a twin of this -- see
   backup/pre-nav-redesign-2026-07-25 -- but moved back inside the
   Morning Brief module as a collapsible card on 2026-07-25.) */
.log-drawer-backdrop{
  position:fixed; inset:0; z-index:19; background:rgba(16,24,40,.28);
  opacity:0; pointer-events:none; transition:opacity .2s ease;
}
.log-drawer-backdrop.open{ opacity:1; pointer-events:auto; }

.log-drawer{
  position:fixed; top:0; right:0; bottom:0; z-index:20; width:420px; max-width:90vw;
  background:var(--bg); border-left:1px solid var(--hairline);
  transform:translateX(100%); transition:transform .2s ease;
  display:flex; flex-direction:column;
}
.log-drawer.open{ transform:none; }
.log-drawer-header{
  display:flex; align-items:center; justify-content:space-between; padding:18px 20px;
  border-bottom:1px solid var(--hairline); flex:0 0 auto;
}
.log-drawer-title{ font-weight:700; font-size:15px; color:var(--ink); }
.log-drawer-close{
  background:none; border:none; font-size:22px; line-height:1; color:var(--ink-soft); cursor:pointer; padding:2px 6px;
}
.log-drawer-close:hover{ color:var(--ink); }
.log-drawer-body{ flex:1 1 auto; overflow-y:auto; padding:20px; }

/* The access-log module still builds its own .band.band-top > .wrap
   shell -- inside a drawer body that just reads as the drawer's own
   content, no double-boxing, since the drawer already provides the
   padding/background. */
.log-drawer-body .band-top{ background:none; box-shadow:none; margin:0; padding:0; }
.log-drawer-body .wrap{ max-width:none; padding:0; margin:0; }
.log-drawer-body .empty-state{ max-width:none; margin:0; padding:8px 0; text-align:left; font-size:14px; }

/* ---- ToDo (modules/todo/module.js) -- rendered inline inside the
   Morning Brief module's own collapsible card now (see that module's
   mountTodoCard), not a drawer -- these rules are shared/generic enough
   to live here rather than duplicated into module.css. */
.todo-form{ display:flex; gap:8px; margin-bottom:16px; }
.todo-form .text-input{ flex:1; }
.todo-panel .text-input{
  font-family:inherit; font-size:13.5px; color:var(--ink);
  background:none; border:1px solid var(--hairline); border-radius:8px; padding:9px 12px;
}
.todo-panel .text-input:focus{ outline:none; border-color:#2F6FE4; }
.todo-panel .empty-state{ max-width:none; margin:0; padding:8px 0; text-align:left; font-size:14px; }

.todo-item{ display:flex; align-items:flex-start; gap:10px; padding:8px 0; border-top:1px solid var(--hairline); }
.todo-list .todo-item:first-child{ border-top:none; }
.todo-check{ width:15px; height:15px; margin-top:3px; accent-color:#2F6FE4; cursor:pointer; flex-shrink:0; }
.todo-body{ flex:1; min-width:0; }
.todo-text{ font-size:14px; line-height:1.4; color:var(--ink); }
.todo-item.todo-done .todo-text{ color:var(--ink-soft); text-decoration:line-through; }

.todo-claude-btn{
  background:none; border:1px solid var(--hairline); border-radius:8px; font-size:14px;
  padding:4px 8px; cursor:pointer; flex-shrink:0; line-height:1;
}
.todo-claude-note{ font-size:12.5px; margin-top:3px; display:flex; align-items:center; gap:8px; }
.todo-claude-pending{ color:var(--status-warn); }
.todo-claude-done{ color:var(--status-ok); }
.todo-claude-clear{
  background:none; border:none; color:var(--ink-soft); font-size:11.5px; text-decoration:underline;
  cursor:pointer; padding:0; font-family:inherit;
}

.todo-done-toggle{
  display:block; margin-top:14px; background:none; border:1px solid var(--hairline); border-radius:8px;
  padding:6px 14px; font-family:inherit; font-size:12.5px; color:var(--ink-soft); cursor:pointer;
}
.todo-done-toggle.active{ background:#2F6FE4; color:#fff; border-color:#2F6FE4; }
.todo-done-list{ margin-top:4px; }
.todo-done-list.hidden{ display:none; }

@media (max-width:640px){
  .security-panel{width:100%;}
  .admin-invite-row,.admin-user-row{ grid-template-columns:1fr; }
  .log-drawer{width:100%;}
  .bottom-bar{ gap:18px; padding:12px 20px; }
}
