@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC;
}

@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-arabic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+200C-200E, U+FB50-FDFF, U+FE70-FEFC;
}

:root {
  --color-bg: #ffffff;
  --color-field-fill: #bfe3fa;
  --color-navy: #14406b;
  --color-text: #14406b;
  --color-text-muted: #4d6b87;
  --color-error: #b3261e;
  /* Shared glass-surface tokens (2026-08-15, second pass). First pass
     used these translucent, hit a real Safari bug (var()-sourced
     backdrop-filter silently failing), got fixed, then got abandoned
     for solid cards out of caution rather than re-tested. Retrying with
     the bug already fixed (every backdrop-filter declaration below
     writes its blur value literally, never through a var()) plus a
     richer background (see .app) for the blur to actually pick up
     colour from -- a flat page was the other real bug, independent of
     the browser. Depth pushed further than the first pass per explicit
     feedback ("work more on the depth and 3d feel"): a bottom inset
     shade plus a tighter contact shadow were added alongside the
     existing top highlight + soft outer lift, so surfaces read as
     physically thick, not just tinted. */
  --card-bg: rgba(255, 255, 255, 0.45);
  --card-bg-strong: rgba(255, 255, 255, 0.55);
  --card-border: rgba(255, 255, 255, 0.75);
  --card-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -1px 0 rgba(20, 64, 107, 0.08),
    0 10px 20px -10px rgba(20, 64, 107, 0.35),
    0 2px 5px -2px rgba(20, 64, 107, 0.18);
  /* Thinner variant (2026-08-15, explicit request: "tasks and calendar
     windows with two lines slightly thinner") -- the top+bottom inset
     pair above reads as two distinct border-like lines on calendar
     cells and task rows specifically, where cells sit edge-to-edge in
     a tight grid/list, more noticeable there than on isolated pills/
     buttons. Same two-line structure, both lines softened rather than
     removed, so the physical-thickness feel stays but reads lighter. */
  /* No inset lines (2026-08-15, third pass): the first "thinner" attempt
     only lowered these same inset highlight/shadow pair's opacity, but
     kept the underlying structure -- a solid 1px border PLUS an inset
     top highlight AND an inset bottom shadow, i.e. three parallel lines
     stacked at each card's edge. Reported back live as "thicker", not
     thinner: softening the inset pair's opacity removed some of the
     blend between them and the solid border, which made the border read
     as a harder, more separated line instead of gently bevelled into the
     card -- more visible, not less. Dropped the inset pair entirely; the
     card's edge is now just the single 1px border plus a soft outer
     shadow for depth. */
  --card-shadow-thin:
    0 6px 14px -10px rgba(20, 64, 107, 0.22),
    0 1px 2px -1px rgba(20, 64, 107, 0.10);
  /* Deeper variant (2026-08-15, explicit request: "more depth and 3d
     feel" on calendar/reminder rows, task rows, and archive message
     rows). NOT a return to the earlier inset-highlight-plus-inset-shadow
     pair (--card-shadow's own inset 0 1px 0 / inset 0 -1px 0 combo) --
     that exact structure was tried on these same dense list/grid
     surfaces before and reverted for reading "thicker", not deeper (see
     the third-pass comment above --card-shadow-thin). This keeps that
     lesson: only ONE inset line (a top highlight, for a touch of sheen),
     never paired with a second inset line at the bottom -- the pairing,
     not inset shadows in general, is what read as a double edge on
     tightly-packed rows/cells. Depth instead comes from a stronger,
     longer-throw OUTER shadow pair (more blur, more spread, higher
     opacity than --card-shadow-thin) -- lift reads through soft shadow
     falloff, not through more lines at the edge. */
  --card-shadow-deep:
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    0 14px 26px -12px rgba(20, 64, 107, 0.38),
    0 4px 10px -4px rgba(20, 64, 107, 0.22);
}

.page,
.app {
  position: relative;
  z-index: 1;
}

* {
  box-sizing: border-box;
}

html {
  height: 100%;
}

body {
  margin: 0;
  min-height: 100%;
  /* Safety net for the safe-area gap (2026-08-15, third pass): stays
     var(--color-bg) (white) rather than changing the locked login
     palette, but ANY sliver of body showing through around .app --
     during the negative-margin overscan above, an overscroll bounce, or
     a device with different safe-area rounding than the ones tested --
     used to expose flat white. #fcfeff is the same near-white .app's own
     top gradient stop now uses, so any such sliver blends in instead of
     reading as a visibly different white. Indistinguishable from
     var(--color-bg) on the login page's white card. */
  background: #fcfeff;
  color: var(--color-text);
  font-family: "Vazirmatn", "Segoe UI", Tahoma, sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* RTL is the document default (set via <html dir="rtl">); this file does not
   mirror per-property with dir() overrides because every layout here is
   authored RTL-first, not flipped from an LTR base. */

.page {
  width: 100%;
  min-height: 100vh;
  /* dvh (dynamic viewport height) as a progressive enhancement over vh --
     see .app below for why; browsers that don't support dvh simply skip
     this declaration and keep the vh line above. */
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--color-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  text-align: center;
}

/* Two-circle brand mark (2026-07-29, replacing the old SVG icon). Pure CSS:
   each circle is a 50px div, positioned with `left`, animated in with
   `transform`/`filter` only -- transform/filter are paint/compositing
   properties that never affect layout size or the `left`/`top` box
   position, so the resting geometry the "to" keyframe settles into is
   exactly the plain, non-animated 50px-at-this-position appearance with no
   residual offset or scale, regardless of how the "from" keyframe looked. */
.login-logo {
  position: relative;
  /* 86x50 == the 50px circle plus the navy circle's 36px offset. Scaled
     down ~10% twice from the original 62px/44px/106x62 spec (2026-07-30:
     62 -> 56 -> 50), each time keeping the overlap ratio proportional
     rather than the absolute overlap: 14/50 == 28.0%, 16/56 == 28.6%,
     18/62 == 29.0%. */
  width: 86px;
  height: 50px;
}

.login-logo-circle {
  position: absolute;
  top: 0;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  animation-duration: 0.9s;
  animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
  animation-fill-mode: both;
}

.login-logo-circle--white {
  left: 0;
  background: #ffffff;
  border: 1px solid #e2e2e2;
  /* Layered for a raised/"popped up" 3D look: a soft drop shadow below,
     a tighter contact shadow, and two inset highlights/shadows for the
     embossed edge. Values are exactly as specified. Note box-shadow never
     affects the element's own box size -- this circle's true diameter is
     exactly 50px, identical to the navy one, verified via
     getBoundingClientRect() in a real browser. If the 16px blur ever
     makes it *perceptually* read as larger than the crisp-edged navy
     circle, reduce only that first blur radius (16px -> ~10-12px); the
     depth effect comes mostly from the layering and the 8px y-offset, so
     it survives a smaller blur. */
  box-shadow:
    0 8px 16px rgba(20, 64, 107, 0.28),
    0 2px 4px rgba(20, 64, 107, 0.18),
    inset 0 -2px 3px rgba(0, 0, 0, 0.06),
    inset 0 2px 3px rgba(255, 255, 255, 0.9);
  animation-name: login-logo-slide-in-left;
}

.login-logo-circle--navy {
  left: 36px;
  background: radial-gradient(circle at 35% 30%, #4a7ba8, #14406b 60%, #0a2740);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
  animation-name: login-logo-slide-in-right;
}

@keyframes login-logo-slide-in-left {
  from {
    transform: translateX(-140px) scale(1.15);
    filter: brightness(2.2);
  }
  to {
    transform: translateX(0) scale(1);
    filter: brightness(1);
  }
}

@keyframes login-logo-slide-in-right {
  from {
    transform: translateX(140px) scale(1.15);
    filter: brightness(2.2);
  }
  to {
    transform: translateX(0) scale(1);
    filter: brightness(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .login-logo-circle {
    animation: none;
  }
}

.brand-name {
  font-family: "Vazirmatn", "Segoe UI", Tahoma, sans-serif;
  font-weight: 700;
  font-size: 1.4rem;
  color: var(--color-navy);
  direction: ltr;
  unicode-bidi: isolate;
}

.login-form {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: right;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field label {
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--color-navy);
}

.field input,
.field textarea,
.field select {
  /* Extended to textarea/select (2026-08-18, پروژه‌ها sheet: هدف needs a
     multi-line field, وضعیت needs a select) -- same fill/radius/padding
     recipe as the existing text inputs, no new visual language. */
  font-family: inherit;
  font-size: 1rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 12px 14px;
  color: var(--color-text);
  outline: none;
}

.field textarea {
  resize: vertical;
  min-height: 60px;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--color-text-muted);
}

.field input:focus,
.field textarea:focus,
.field select:focus {
  box-shadow: 0 0 0 2px var(--color-navy);
}

.submit-button {
  font-family: inherit;
  font-size: 1.05rem;
  font-weight: 700;
  background: var(--color-navy);
  color: #ffffff;
  border: none;
  border-radius: 10px;
  padding: 14px 18px;
  min-height: 44px;
  cursor: pointer;
  /* Stronger depth recipe (2026-08-16, second pass -- --card-shadow alone
     still read as flat on these solid, saturated CTA buttons; its inset
     highlight is only 1px and tuned for pale translucent surfaces like
     .range-pill, not a small solid navy/green button). Switched to the
     same recipe already proven on .record-fab/.quick-add-fab -- a
     brighter, thicker inset top sheen plus an inset bottom shadow line
     plus a real lifted outer shadow -- which nobody has complained looks
     flat. Every primary CTA button in the app shares this class, so this
     one change covers ذخیره/تأیید/تایید everywhere at once. */
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.35),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 10px 18px -8px rgba(20, 64, 107, 0.45);
  /* Without this, iOS Safari's native button chrome can render its own
     platform tint over an author background-color (2026-07-29: this is
     exactly why #confirm-proposal rendered blue-ish on a real iPhone
     instead of the intended green/grey -- not reproducible in Chromium,
     where author styles already win outright, which is why this went
     unnoticed until real device testing). Harmless everywhere else. */
  -webkit-appearance: none;
  appearance: none;
}

.submit-button:hover {
  opacity: 0.92;
}

.error-message {
  color: var(--color-error);
  font-size: 0.9rem;
  text-align: center;
}

.shell-page {
  width: 100%;
  max-width: 480px;
  padding: 32px 24px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: center;
}

.logout-link {
  color: var(--color-navy);
  font-size: 0.95rem;
}

/* Voice capture (Phase B) */

.voice-panel {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  text-align: right;
}

.voice-panel h2 {
  font-size: 1.05rem;
  color: var(--color-navy);
  margin: 0;
  text-align: center;
}

/* Record | Confirm sit in a persistent two-column row (2026-07-29). Record
   left, Confirm right, achieved entirely through the page's own native RTL
   flex flow: in `dir="rtl"`, a plain `display: flex` row places the first
   DOM child at the "start" edge, which is the RIGHT in RTL -- so
   confirm-proposal is listed FIRST in home.html (rendering right) and
   record-button SECOND (rendering left). An earlier version of this rule
   instead isolated the row with `direction: ltr` (mirroring .brand-name's
   isolation of its own LTR brand text) and put record-button first --
   reverted the same night after a real iPhone Safari test showed the two
   buttons together only spanning roughly half the container's width, not
   reproducible in Chromium at either desktop or mobile viewport sizes. The
   direction-override + flex:1 + min-width:0 combination is exactly the
   kind of interaction real engines have historically diverged on for RTL
   flex sizing; native same-direction flow removes that axis of risk
   entirely rather than trying to guess which part of it Safari handled
   differently. */
.action-row {
  display: flex;
  gap: 12px;
}

.action-row .submit-button {
  flex: 1;
  min-width: 0;
}

.confirm-button {
  background: #2e7d32;
}

.confirm-button:disabled {
  /* Native :disabled, not a JS-toggled class -- the button's actual
     clickability and its visual state can never drift out of sync with
     each other, unlike a manually-toggled class would allow. */
  background: var(--color-text-muted);
  opacity: 0.6;
  cursor: not-allowed;
}

/* Restore-row layout (2026-08-16, iterated twice on the same day from
   live-testing feedback):
   - First pass: label got its own full-width line (item-row-content--
     restore, column layout below), because the original single
     horizontal line packed date+time+label+two text buttons into one
     row and squeezed the task text to one word per line.
   - Second pass ("on top of each other"): بازگردانی/حذف stacked
     vertically instead of side by side, to save horizontal width.
   - Third pass ("fit all deleted messages in one or two lines, 2
     messages fill the whole screen"): the vertical button stack from
     pass two fixed width but cost too much HEIGHT -- reverted to small
     round ICON-only buttons living on the same line as date/time
     instead of their own block, so each row is back to label line +
     one meta/actions line, not three. */
.item-row-content--restore {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.item-row-restore-meta {
  display: flex;
  align-items: center;
  gap: 10px;
}

.item-row-restore-icon {
  /* Pushes both icon buttons to the row's trailing edge, date/time stay
     at the leading edge -- only needs to be set on the first one since
     it's a margin on the flex item, not the container. */
  margin-inline-start: auto;
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #ffffff;
  -webkit-appearance: none;
  appearance: none;
}

.item-row-restore-icon svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

.item-row-restore-icon--restore {
  background: #2e7d32;
  margin-inline-start: auto;
}

.item-row-restore-icon--danger {
  background: var(--color-error, #c62828);
  margin-inline-start: 0;
}

.item-row-restore-icon:disabled {
  background: var(--color-text-muted);
  opacity: 0.6;
  cursor: not-allowed;
}

/* Real bug, reported 2026-08-15: .sheet has no height limit of its own,
   and #restore-list (a plain .item-list, normally used inside a full,
   independently-scrolling .panel) inherited that same unbounded growth --
   with enough recently-completed items the sheet grew taller than the
   viewport, and since .sheet-backdrop is a fixed, non-scrolling flex
   container (align-items: flex-end), the top of the sheet -- heading,
   note, and the ✕ close button -- was pushed off-screen with no way to
   scroll back up to it. Force-quitting the app was the only way out.
   Bounding the LIST (not the whole sheet) keeps heading/note/close
   always on-screen and only the rows themselves scroll underneath. */
#restore-list {
  gap: 8px;
  max-height: 50vh;
  overflow-y: auto;
}

/* Real bug, reported 2026-08-15 (live-verified via Chrome): #restore-list
   is still `.item-list`'s `display: flex; flex-direction: column`, and a
   flex column with a bounded max-height flex-shrinks its children to fit
   by default -- overflow-y: auto does NOT stop that shrinking on its own,
   only scrolling does once shrinking hits its floor. With 25 rows needing
   far more than 50vh, every row got crushed to ~7px tall (data and text
   were genuinely all still there, just visually squashed to a sliver --
   confirmed via getBoundingClientRect before this fix). flex-shrink: 0
   makes rows keep their natural height and actually scroll instead. */
#restore-list > .item-row {
  flex-shrink: 0;
}

.voice-status {
  min-height: 1.2em;
  margin: 0;
  font-size: 0.9rem;
  color: var(--color-text-muted);
  text-align: center;
}

.voice-status--error {
  color: var(--color-error);
}

.voice-status--ok {
  color: var(--color-navy);
}

.voice-result {
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: var(--color-field-fill);
  border-radius: 10px;
  padding: 16px;
}

.result-block h3 {
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-navy);
  margin: 0 0 4px;
}

.result-text {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--color-text);
  white-space: pre-wrap;
  /* Transcripts may mix Persian with Latin names or numbers; keep the
     paragraph RTL and let the bidi algorithm place the runs. */
  text-align: right;
}

/* Commitment proposal card */

.proposal-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: #e8f5e9;
  border-radius: 10px;
  padding: 16px;
  border-inline-start: 4px solid #2e7d32;
}

.proposal-card h3 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-navy);
  margin: 0 0 4px;
  text-align: center;
}

.proposal-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* Fixed 2026-07-29: an author-stylesheet `display` declaration always wins
   over the user-agent stylesheet's implicit `[hidden] { display: none }`,
   regardless of selector specificity or source order -- origin/importance is
   sorted before specificity in the cascade. Without this rule,
   `#proposal-clarification-block.hidden = true` (set correctly by voice.js)
   had zero visual effect: the block, and any stale clarification text left
   over from an earlier ambiguous proposal in the same page session, stayed
   visibly rendered underneath every later confirmable proposal. Verified in
   a real browser: computed `display` was `flex` (element visible) before
   this rule, `none` (correctly hidden) after. */
.proposal-item[hidden] {
  display: none;
}

.proposal-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: #1b5e20;
  text-align: right;
}

.proposal-value {
  margin: 0;
  font-size: 0.95rem;
  color: var(--color-text);
  text-align: right;
  line-height: 1.5;
}


/* ============================================================
   Mobile app shell (2026-07-30 rebuild): full-screen Home with a
   header, range switcher, combined item list, floating record
   button and a bottom nav bar. The login page keeps the older
   centred-card layout above; this section is additive.
   ============================================================ */

.app {
  /* Coloured, not flat (2026-08-15, second pass). --color-bg (used by
     body and, deliberately, the login page's white card) stays
     untouched -- that white is a locked decision. A single flat tint
     (#f0f5fa) was tried first and was already an improvement over pure
     white, but still gave the blur almost nothing to actually pick up
     -- translucent-white cards on a near-flat backdrop read as barely
     tinted white, not "glass." Real glass/blur only looks convincing
     over something with genuine colour variation behind it (this is
     exactly why the reference farming-app screenshot works: the cards
     blur an actual photo). No photo fits this app's brand, so this
     builds the same effect from the existing palette instead: several
     soft radial washes of the brand colours, layered over a light
     base gradient. */
  /* Brightened (2026-08-15, explicit request): lighter base gradient
     stops, and the navy wash (the one colour actively darkening the
     canvas rather than just tinting it) pulled back slightly -- same
     colour composition otherwise. */
  /* Top 20% brightened further (2026-08-15, third pass): the previous
     brightening pass lightened the base gradient and pulled the navy
     radial's opacity down, but never touched WHERE that radial sits --
     it's centred at 100% 8% with a 90%-wide / 50%-tall spread, so its
     strongest point is still inside the very top band the user is
     looking at. Pushed its centre down to 20% and cut its opacity again
     (0.16 -> 0.09) so the top strip sits mostly outside its hottest
     zone, and lightened the base linear-gradient's top stop to a near-
     white blue instead of #f7fbff. */
  /* Top white fade (2026-08-15, fourth pass): the negative-margin overscan
     trick below was meant to let this box's colour reach behind the real
     status bar/dynamic-island, but on the actual device that strip still
     renders as plain white at rest (only the overscroll bounce shows the
     gradient) -- and the first radial below is centred at 15% 0%, i.e. its
     BLUEST point sits exactly on that boundary, so wherever the hack falls
     short you get flat white butting straight into the most saturated blue
     in the whole gradient: the hardest possible seam. Rather than chase the
     hack further, make the seam invisible on its own terms -- an opaque
     #fcfeff (matches body's safety-net white exactly) at y=0 fading to
     transparent by 140px, painted ABOVE the radials. Whether or not the
     overscan hack is actually reaching the notch on this device, the pixels
     right at the top are now white either way, and the blue only becomes
     visible after it's already fading in gradually underneath this layer --
     a real light-blue-to-white gradient instead of a hard edge. */
  /* Background darkened ~20% (2026-08-15, explicit request, "just tonight"
     scope): only the four colour-wash radials' own alpha was raised
     (×1.2 each) -- the top white-fade layer above them and the base
     linear-gradient's white top/bottom anchor stops (#fcfeff / #f0f6fc)
     are untouched on purpose. Those two are load-bearing: the fade layer
     is what keeps the notch/status-bar seam invisible (see its own
     comment above), and the base gradient's white anchors are what keep
     the very top and bottom of the page reading light regardless of how
     saturated the middle gets. Darkening those would have undone both
     fixes for a cosmetic change unrelated to either. */
  /* Another 30% darker on top of that (2026-08-15, explicit request, to
     see whether the new --card-shadow-deep surfaces read as depth against
     a richer backdrop) -- same rule as before: only these four radials'
     alpha (×1.3 on top of the already-×1.2 values), top fade layer and
     base gradient's white anchors still untouched. */
  background:
    linear-gradient(180deg, #fcfeff 0%, rgba(252, 254, 255, 0) 140px),
    radial-gradient(120% 60% at 15% 0%, rgba(191, 227, 250, 0.86), transparent 55%),
    radial-gradient(90% 55% at 100% 20%, rgba(20, 64, 107, 0.14), transparent 55%),
    radial-gradient(80% 45% at 88% 62%, rgba(46, 125, 50, 0.25), transparent 55%),
    radial-gradient(70% 40% at 8% 78%, rgba(249, 168, 37, 0.31), transparent 55%),
    linear-gradient(180deg, #fcfeff 0%, #f0f6fc 100%);
  background-attachment: fixed;
  /* body is flex-centred, so this stays horizontally centred while
     still filling the viewport height like a native app shell. */
  width: 100%;
  max-width: 480px;
  /* Overscan into the safe area (2026-08-15, third pass): viewport-
     fit=cover + the safe-area-inset-top padding below was meant to let
     this box's own background paint behind the notch/status bar, but
     that strip is still reported as plain white by default on the real
     device (only briefly filling in during overscroll bounce) -- which
     means .app's box isn't actually reaching that area at rest, likely
     a sub-pixel/vh rounding gap between this box and the true top of
     the safe area, with body's own flat white background showing
     through the gap. A negative top margin equal to the inset pulls
     this box's top edge (and its background) up past the nominal
     viewport top by that same amount, guaranteeing overlap regardless
     of any rounding; the matching padding increase below keeps the
     actual header content shifted back down to where it was. */
  margin-top: calc(-1 * env(safe-area-inset-top, 0px));
  min-height: 100vh;
  /* Real bug, 2026-08-14: "the bottom toolbar minimizes/disappears when
     swiping down" -- traced to plain vh's well-known mobile-Safari
     quirk: 1vh is computed against the LARGEST possible viewport (as if
     the browser's own address-bar chrome were already hidden), not the
     current visible one. With .bottom-nav pinned via position:fixed
     inside a min-height:100vh container, Safari's chrome hiding/showing
     on scroll changes the ACTUAL visible height while vh stays fixed
     against the larger one, so the fixed nav can visually shift/appear
     to hide -- a platform interaction, not anything this app's JS ever
     coded (confirmed: no scroll listener or hide-on-scroll logic exists
     anywhere in app.js, and .bottom-nav's own position/bottom rules are
     unchanged from before tonight). dvh (dynamic viewport height) tracks
     the real visible viewport instead; kept as a progressive
     enhancement (declared second) so browsers without dvh support
     simply keep the vh line above. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* bottom nav's own content height (56px -> 48px sixth pass -> 28px
     seventh pass, 2026-08-15 -- icon size/position unchanged, just less
     bar padding around them) + breathing room + the same
     safe-area-inset-bottom the nav itself now reserves below that (see
     .bottom-nav) -- without this, scrollable content's last row would
     sit partly behind the taller, safe-area-extended nav bar. Keep this
     number in sync with .bottom-nav's height if that ever changes again.
     Top padding also extended by safe-area-inset-top (2026-08-15,
     explicit request): with viewport-fit=cover now set in base.html,
     this box's own background (which now fills the space this padding
     reserves) can finally paint behind the notch/dynamic-island strip,
     while this padding keeps the actual header content (logo/title)
     shifted safely below it. Doubled to 2x safe-area-inset-top
     (2026-08-15, third pass): pairs with the new negative top margin
     above -- that margin pulls the box up by one inset's worth, so the
     padding needs a second inset's worth on top of the original 20px to
     keep the header sitting where it was instead of shifting up too. */
  padding: calc(20px + (2 * env(safe-area-inset-top, 0px))) 18px calc(28px + 20px + env(safe-area-inset-bottom, 0px));
  text-align: right;
}

.app-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.app-logo {
  position: relative;
  width: 38px;
  height: 22px;
  flex: none;
}

.app-logo-circle {
  position: absolute;
  top: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
}

/* Confirm listed FIRST in the markup so RTL flex/absolute order puts
   white on the LEFT and the outlined circle on the RIGHT, matching the
   brand mark's intended orientation. Positions are explicit `left`
   values, so this is orientation-stable regardless of text direction. */
.app-logo-circle--white {
  left: 0;
  background: #ffffff;
  border: 1px solid #e2e2e2;
  box-shadow: 0 1px 3px rgba(20, 64, 107, 0.22);
}

.app-logo-circle--outline {
  left: 16px;
  background: transparent;
  border: 2.5px solid var(--color-navy);
}

.app-title {
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--color-navy);
  /* Latin brand name inside an RTL document -- same isolation the
     login page's .brand-name uses. */
  direction: ltr;
  unicode-bidi: isolate;
}

/* --- Home-card widget (2026-08-15) ---
   A single glass surface, ~half the viewport tall on a typical phone,
   sitting above the range pills on the Home tab only. Its own background
   swaps by time of day via [data-tod] (set/updated by home-card.js) --
   separate from .app's own background gradient below it, so the card
   reads as its own weather-app-like surface rather than blending into the
   page chrome. */

.home-card {
  position: relative;
  border-radius: 22px;
  border: 1px solid var(--card-border);
  padding: 18px;
  min-height: 46vh;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: hidden;
  color: #ffffff;
  /* Written literally, not through var() -- see the shared glass-surface
     token comment above (.range-pill) for the real Safari bug this
     avoids. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    0 16px 30px -14px rgba(20, 64, 107, 0.45);
  transition: background 1.2s ease;
}

/* Time-of-day backgrounds (2026-08-19, swapped the earlier 7-band photo
   set for a flat-illustration set the user picked after a free-tool
   preview pass -- 4 bands now, matched 1:1 to the 4 files in
   /static/img/weather/, picked client-side by home-card.js from the
   visitor's local clock via [data-tod]. See CLAUDE.md for the full
   thread (Nano Banana polish pass still pending, separate work). PNG
   (not the smaller WebP) for guaranteed broad rendering, cached by the
   browser after first load so this is a one-time cost, not per-view. */
/* Each rule layers a dark scrim UNDER the art isn't right -- gradients
   and images are both background-image layers, first listed paints on
   TOP, so the scrim is listed first here to sit above the art. Needed
   because the white card text (temp/date/tiles) would be unreadable
   against the brighter morning/day art otherwise; the scrim is
   deliberately light (not present in the original art) so the
   illustration still reads through clearly. */
.home-card[data-tod="morning"] {
  background-image: linear-gradient(180deg, rgba(20, 30, 50, 0.22), rgba(20, 30, 50, 0.38)), url("/static/img/weather/01_morning.png");
}
.home-card[data-tod="day"] {
  background-image: linear-gradient(180deg, rgba(15, 30, 55, 0.2), rgba(15, 30, 55, 0.35)), url("/static/img/weather/02_day.png");
}
.home-card[data-tod="evening"] {
  background-image: linear-gradient(180deg, rgba(20, 20, 45, 0.18), rgba(20, 20, 45, 0.35)), url("/static/img/weather/03_evening.png");
}
.home-card[data-tod="night"] {
  background-image: linear-gradient(180deg, rgba(0, 0, 15, 0.1), rgba(0, 0, 15, 0.3)), url("/static/img/weather/04_night.png");
}
.home-card[data-tod] {
  background-size: cover, cover;
  background-position: center, center;
}

/* Profile button/photo + popover removed entirely 2026-08-17 (explicit
   request: "remove the kamyar ghaedpour member of the board card") --
   row-top now holds only the location label. */
.home-card-row-top {
  display: flex;
  align-items: flex-start;
  /* space-between (2026-08-20): now holds two labels, city on one end
     and painting attribution on the other -- was flex-start back when
     this row only ever had the single location label. */
  justify-content: space-between;
  gap: 12px;
}

.home-card-location-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.home-card-location {
  /* Smaller, not bold (2026-08-20 explicit request) -- was 0.95rem/600. */
  font-size: 0.8rem;
  font-weight: 400;
  opacity: 0.85;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Avatar (2026-08-20, explicit request): small circular photo beneath the
   city label, styled like a typical weather-app profile avatar -- a thin
   light ring, no name/occupation text alongside it. */
.home-card-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  border: 1.5px solid rgba(255, 255, 255, 0.7);
}

/* Painting attribution (2026-08-20): painter/title for the Met Museum
   background art, top-left, sized to visually mirror the city label on
   top-right rather than matching its internal weight line for line.
   English text inside an RTL document -- same direction:ltr;
   unicode-bidi:isolate pattern already used for .brand-name elsewhere,
   so "Achille-Etna Michallon" reads left-to-right instead of getting
   bidi-reordered. */
.home-card-art-credit {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: left;
  max-width: 60%;
}

.home-card-art-painter {
  font-size: 0.8rem;
  font-weight: 500;
  opacity: 0.85;
}

.home-card-art-title {
  font-size: 0.72rem;
  font-weight: 400;
  font-style: italic;
  opacity: 0.75;
  margin-top: 1px;
}

/* Date line (2026-08-17, explicit request): sits above the temperature,
   Jalali "day month year" (e.g. "۲۶ مرداد ۱۴۰۵"), built client-side in
   home-card.js from the same /calendar/meta payload the wheel date
   picker already uses -- no new endpoint needed. */
.home-card-date {
  text-align: center;
  font-size: 0.8rem;
  opacity: 0.8;
  margin-top: 4px;
}

/* Weather block (2026-08-17, explicit request: temperature made smaller
   and pushed lower -- was 2rem sitting right at the top next to the
   profile photo, now a standalone centered block below the date, with
   top margin doing the "lower" part). */
.home-card-weather-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 18px;
}

/* Weather illustration icon (2026-08-17, second pass): the user's real
   transparent icon set, mapped off condition_icon + is_day in
   home-card.js's iconFilename(). Sits above the temperature; stays
   `hidden` until a real icon resolves so nothing renders as a broken
   image while /home-card/weather is still loading. */
.home-card-weather-icon {
  width: 64px;
  height: 64px;
  object-fit: contain;
  margin-bottom: 2px;
}

.home-card-temp {
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.1;
}

.home-card-condition {
  font-size: 0.8rem;
  opacity: 0.85;
  margin-top: 2px;
}

.home-card-sun {
  display: flex;
  justify-content: center;
  gap: 28px;
}

.home-card-sun-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.85rem;
  opacity: 0.9;
}

.home-card-sun-item svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

.home-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: auto;
}

.home-card-tile {
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 14px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.home-card-tile-label {
  font-size: 0.72rem;
  opacity: 0.85;
}

.home-card-tile-value {
  font-size: 1.15rem;
  font-weight: 700;
}

.home-card-tile-sub {
  font-size: 0.72rem;
  opacity: 0.85;
}

/* Date/week tile split into two real boxes (2026-09-09, explicit
   request, revised same day): the date/week grid cell now holds a pair
   of actual .home-card-tile pills instead of one -- 2/3 width for the
   existing date+week content, 1/3 width left empty as a reserved slot
   for later, with a visible gap between them so it reads as genuinely
   split (the first attempt, one box internally divided by flex with no
   border between the halves, didn't look split at all). The aqi tile
   elsewhere in the grid is untouched. */
.home-card-tile-pair {
  display: flex;
  gap: 10px;
  min-width: 0;
}

.home-card-tile--date {
  flex: 2;
  min-width: 0;
}

.home-card-tile--empty {
  flex: 1;
  min-width: 0;
}

.home-card-rates {
  display: flex;
  gap: 10px;
}

.home-card-rate {
  flex: 1;
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 14px;
  padding: 8px 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.85rem;
}

.home-card-rate-label {
  opacity: 0.85;
  font-weight: 600;
}

.home-card-rate-value {
  font-weight: 700;
  direction: ltr;
}

/* Profile popover removed entirely 2026-08-17 (explicit request). */

/* --- Range switcher (روز / هفته / ماه) --- */

.range-pills {
  display: flex;
  gap: 8px;
}

.range-pill {
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  flex: 1;
  padding: 8px 10px;
  border: 1px solid var(--card-border);
  border-radius: 999px;
  background: var(--card-bg);
  /* Written literally, never through a var() (2026-08-15): the actual
     Safari bug this app hit was -webkit-backdrop-filter silently
     failing when its value came from a CSS custom property. Every
     backdrop-filter declaration in this file follows that rule now. */
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow);
  color: var(--color-text-muted);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.range-pill.is-active {
  background: rgba(20, 64, 107, 0.72);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  border-color: rgba(255, 255, 255, 0.35);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.3), inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 10px 18px -6px rgba(20, 64, 107, 0.5);
  color: #ffffff;
}

/* --- Home motto (2026-08-30, explicit request) --- */

.home-motto {
  /* Symmetric left/right padding (not a flex split with the edit
     button) is what keeps the text mathematically centered in the
     card -- the button sits absolutely positioned inside the left
     padding gutter, so it never shifts the text's own centered box. */
  position: relative;
  padding: 14px 42px;
  border-radius: 16px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow);
}

.home-motto-text {
  margin: 0;
  min-height: 1.2em;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--color-text);
  text-align: center;
  overflow-wrap: break-word;
}

.home-motto-edit-toggle {
  position: absolute;
  top: 50%;
  left: 10px;
  transform: translateY(-50%);
}

.home-motto-edit-row {
  margin-top: 0;
}

/* --- گذشته bulk action bar (Home, range=past only) --- */

.bulk-action-bar {
  display: flex;
  align-items: center;
  gap: 8px;
}

.bulk-select-all {
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-navy);
  background: none;
  border: none;
  padding: 4px 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.bulk-action-count {
  flex: 1;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.bulk-action-button {
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow);
  color: var(--color-navy);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.bulk-action-button:disabled {
  opacity: 0.45;
  cursor: default;
}

.bulk-action-button--complete {
  color: #ffffff;
  background: #2e7d32;
  border-color: #2e7d32;
}

/* Icon-only bulk bar (2026-08-18, replaces the انتخاب همه/تکمیل شد text
   buttons -- explicit request: "too big"). Small (30px) round buttons,
   same visual weight as other compact icon controls in this app (see
   .archive-image-thumb-delete). .bulk-select-all/.bulk-action-button
   above are now dead CSS kept only because .bulk-action-count is still
   shared -- not worth a churny rename. */
.bulk-icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--color-navy);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  padding: 0;
}

.bulk-icon-button svg {
  width: 16px;
  height: 16px;
}

.bulk-icon-button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* در انتظار bulk رد/پذیرش (2026-09-07): coloured + differently sized so
   the two are never confusable at a glance -- رد (destructive-feeling,
   reversible) larger and red; پذیرش (creates real tasks/events) a plain
   neutral outline, deliberately NOT the green/blue "safe" tone the rest
   of the app uses for تکمیل‌شده. */
.waiting-bulk-reject-icon {
  width: 34px;
  height: 34px;
  border-color: #c62828;
  color: #c62828;
}

.waiting-bulk-reject-icon svg {
  width: 18px;
  height: 18px;
}

.waiting-bulk-accept-icon {
  color: var(--color-text-muted);
}

/* Select-all icon, active state (2026-09-07): highlighted while در انتظار
   is in selection mode, so it's visually obvious the dots are temporarily
   checkboxes rather than colour pickers. */
#waiting-bulk-select-all.is-active {
  background: var(--color-navy);
  color: #ffffff;
  border-color: var(--color-navy);
}

.bulk-action-icons {
  display: flex;
  gap: 6px;
}

/* Move-to-another-day picker (2026-08-18, explicit request): appears
   below the bar when جابجایی is tapped, replacing what would otherwise
   be a separate menu step -- just one swipeable horizontal day strip
   (built in app.js) and a confirm button. Styled like the existing
   .wheel-picker (same field-fill background) rather than a new look. */
.bulk-move-picker {
  background: var(--color-field-fill);
  border-radius: 12px;
  padding: 10px;
  margin-top: 6px;
}

.bulk-move-month-label {
  text-align: center;
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}

.bulk-move-strip {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 4px;
  padding: 4px 0;
  scrollbar-width: none;
}

.bulk-move-strip::-webkit-scrollbar {
  display: none;
}

.bulk-move-strip-item {
  flex: 0 0 38px;
  scroll-snap-align: center;
  text-align: center;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-text-muted);
  padding: 8px 0;
  border-radius: 8px;
  direction: ltr;
  unicode-bidi: isolate;
}

.bulk-move-strip-item.is-selected {
  color: var(--color-navy);
  background: var(--card-bg-strong);
  font-size: 0.95rem;
}

.bulk-move-confirm {
  width: 100%;
  margin-top: 8px;
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 8px;
  border-radius: 8px;
  border: none;
  background: var(--color-navy);
  color: #ffffff;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* Selection circle (2026-08-14): replaces the plain white-square checkbox
   AND the separate colored dot/border-left type marker that used to sit
   elsewhere on the same row -- one element now serves both jobs. Always
   filled with --item-accent (green=task, blue=calendar), so the type is
   visible whether or not the row is selected; a checkmark appears inside
   only when selected. */
.item-select-circle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 17px;
  height: 17px;
}

.item-select-circle-input {
  appearance: none;
  -webkit-appearance: none;
  box-sizing: border-box;
  width: 17px;
  height: 17px;
  margin: 0;
  border-radius: 50%;
  /* 1px border (2026-08-15, added for the new "white" priority color --
     without it a white dot vanishes entirely against the page's own white
     background). box-sizing: border-box keeps the total size at 17px, so
     this is purely a visibility fix, not a size change. */
  border: 1px solid var(--color-field-fill);
  background: var(--item-accent, var(--color-text-muted));
  cursor: pointer;
  position: relative;
}

.item-select-circle-input::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 44%;
  width: 4px;
  height: 8px;
  border: solid #ffffff;
  border-width: 0 2px 2px 0;
  transform: translate(-50%, -50%) rotate(45deg);
  opacity: 0;
}

.item-select-circle-input:checked::after {
  opacity: 1;
}

/* White priority color (2026-08-18 report): the checkmark above is white,
   so on a white-filled circle it was invisible when checked -- couldn't
   tell if the item was done or not. Only this variant gets a dark
   checkmark; every other accent color keeps the white one. */
.item-select-circle-input--on-white::after {
  border-color: #1c1c1c;
}

/* Project-linked checkbox (2026-08-18 follow-up, replaces the separate
   folder badge above): "its better to replace folder with bullet point,
   put folder with the possibility of checking inside instead of bullet
   points, so you can remove brown color" -- the checkbox itself is
   clipped into a folder silhouette instead of a circle when the item is
   linked to a project, so it's still tappable/checkable exactly like the
   plain circle, just shaped differently. No separate color is needed
   since the shape alone is the signal (project color removed entirely). */
.item-select-circle-input--folder {
  border-radius: 3px 3px 2px 2px;
  clip-path: polygon(
    0% 22%, 40% 22%, 48% 32%, 100% 32%,
    100% 100%, 0% 100%
  );
}

.item-select-circle-input--folder::after {
  top: 60%;
}

/* --- Combined item list --- */

.item-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

.item-list-empty {
  margin: 12px 0 0;
  font-size: 0.9rem;
  color: var(--color-text-muted);
  text-align: center;
}

.item-list-empty[hidden] {
  display: none;
}

/* Swipe-to-delete (2026-08-14): .item-row is now the outer, fixed
   container; .item-row-content holds the actual visible row and is what
   translates horizontally on drag, revealing .item-row-delete underneath
   it. Every row across Home, the Calendar list, and the Tasks tab is
   built through wrapRowForSwipe() in app.js, so this structure is shared
   rather than duplicated per surface. */
.item-row {
  position: relative;
  overflow: hidden;
  border-radius: 10px;
}

.item-row-content {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  /* Full glass treatment, finally (2026-08-15, "not even close to the
     pills/تکمیل‌شد button" report): this row used to stay permanently
     opaque because .item-row-delete sat directly behind it, always
     rendered red -- any translucency here bled a visible pink tint at
     rest, confirmed live twice. That's genuinely why this never matched
     the depth of .range-pill/.bulk-action-button--complete: those are
     translucent+blurred over the coloured .app background underneath
     (that's most of where their "pop" actually comes from, not just the
     box-shadow), while this was a flat opaque card no matter what shadow
     was applied to it. Root fix is in app.js's setOffset(), not here:
     .item-row-delete is now colourless except while actually revealed
     (see its own CSS below), so there's nothing red behind this at rest
     to bleed through -- same background/border/shadow recipe as every
     other glass surface in this file now, not a special case. */
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow);
  border-radius: 10px;
  position: relative;
  z-index: 1;
  touch-action: pan-y;
  transition: transform 0.15s ease-out;
}

.item-row-content.is-dragging {
  transition: none;
}

.item-row-delete {
  position: absolute;
  /* inset: 2px, not 0 (2026-08-15 fix) -- with this at inset:0 and the
     exact same border-radius/box as .item-row-content sitting directly
     on top of it, the two are independently-painted/composited layers
     that can rasterize their shared rounded-corner curve a fraction of
     a device pixel apart. That sub-pixel seam let this red button's
     colour show through as a thin line tracing every row's edge even
     at rest, worst at the rounded corners -- reported live as "red
     bent lines" on every task/reminder row, confirmed by hiding this
     element entirely and watching the line disappear. A 2px inset
     margin (plus a slightly smaller radius to match) gives
     .item-row-content real overlap to fully cover this element with
     room to spare, instead of relying on two separate layers landing
     on the exact same sub-pixel boundary. Doesn't change the reveal
     behaviour at all -- JS still drives the swipe via
     .item-row-content's own translateX, this only affects the 2px
     margin around the always-present, normally-hidden button. */
  inset: 2px;
  display: flex;
  align-items: center;
  /* flex-start, not flex-end: the app is RTL (dir="rtl"), where
     flex-start resolves to the visual RIGHT edge -- exactly the side that
     becomes visible when .item-row-content slides LEFT (negative
     translateX) to reveal this button underneath it. flex-end would put
     this on the visual LEFT instead, the side that's still covered. */
  justify-content: flex-start;
  padding-inline-start: 18px;
  border: none;
  border-radius: 8px;
  /* Colourless at rest, red only while revealed (2026-08-15): this is the
     actual fix that lets .item-row-content go translucent above -- see
     its own comment. Keyed off .item-row--open, toggled in app.js's
     setOffset() for any non-zero offset (mid-drag or pinned open), not
     off :hover/:focus -- this is a touch-swipe reveal, not a pointer
     hover state. Transition is intentionally quick/linear-ish (not the
     row's own slower slide easing) so the colour is already there by the
     time a drag has moved enough to expose any of this button's area. */
  background: transparent;
  color: transparent;
  transition: background-color 0.1s ease, color 0.1s ease;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.item-row--open .item-row-delete {
  background: var(--color-error, #c62828);
  color: #ffffff;
}

.item-row-time {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--color-navy);
  flex: none;
  min-width: 3.6em;
  /* Digits are already Persian-Arabic from the server; keep them
     rendering as one LTR-ordered clock value inside the RTL row. */
  direction: ltr;
  unicode-bidi: isolate;
  text-align: right;
}

.item-row-time:empty::before {
  content: "—";
  color: var(--color-text-muted);
}

.item-row-label {
  font-size: 0.95rem;
  line-height: 1.45;
  color: var(--color-text);
  flex: 1;
  min-width: 0;
}

/* A project-linked task finished elsewhere still shows here, checked
   (2026-08-18 explicit request) -- dimmed + struck through so it reads as
   a completed record, not an active item to tap. */
.item-row-content--done .item-row-label {
  color: var(--color-text-muted);
  text-decoration: line-through;
}

.item-row-content--done .item-row-time,
.item-row-content--done .item-row-date {
  color: var(--color-text-muted);
}

/* Per-source accent colours. Adding WhatsApp/CRM/etc. later is one new
   rule here plus the matching `source` string from the server -- no JS
   or template change, which is what keeps this extensible. */
.item-row--task { --item-accent: #2e7d32; }      /* commitments / tasks */
.item-row--calendar { --item-accent: #1565c0; }  /* calendar events */

/* در انتظار (2026-08-15): a plain intro line + review-queue rows. Each
   row reuses .item-row/.item-row-content's own glass recipe (via
   .waiting-row/.waiting-row-content below) for the same depth as every
   other list surface, but isn't built through wrapRowForSwipe() -- there's
   no swipe gesture here, just explicit پذیرش/رد buttons and a destination
   select, so the row stacks its content vertically instead of the usual
   horizontal single-line layout. */
.waiting-intro {
  font-size: 0.82rem;
  color: var(--color-text-muted);
  line-height: 1.5;
  margin: 0 2px 4px;
}

.waiting-row {
  border-radius: 10px;
}

.waiting-row-content {
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}

/* One dot per row (2026-09-07, replaces the plain selection checkbox):
   doubles as the colour chooser (default state = the same rainbow conic-
   gradient as the colour filter's "همه رنگ‌ها" swatch, meaning "no colour
   picked yet"; tapping it opens that same popover in "assign" mode) and,
   only while selection mode is active, a temporary bulk-select checkbox.
   Sized like .item-select-circle-input for visual consistency with every
   other row's selection circle, but it's a <button> (opens a popover),
   not an <input type=checkbox>. */
.waiting-row-dot {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 17px;
  height: 17px;
  min-width: 17px;
  margin: 0;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--color-field-fill);
  background: conic-gradient(#c62828, #f9a825, #2e7d32, #1565c0, #c62828);
  cursor: pointer;
  position: relative;
}

.waiting-row-dot--selection {
  background: var(--color-text-muted);
}

.waiting-row-dot--selection::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 44%;
  width: 4px;
  height: 8px;
  border: solid #ffffff;
  border-width: 0 2px 2px 0;
  transform: translate(-50%, -50%) rotate(45deg);
  opacity: 0;
}

.waiting-row-dot--selection.is-checked::after {
  opacity: 1;
}

.waiting-row-context {
  font-size: 0.82rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}

.waiting-row-source {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--color-text-muted);
  line-height: 1.3;
}

.waiting-row-meta {
  display: flex;
  gap: 8px;
}

.waiting-row-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Sits inside the card now (see buildWaitingRow in app.js), so the card's
     own 11px/12px padding applies -- only a little breathing room above the
     buttons is needed here, not the old outside-the-card compensation. */
  padding: 6px 0 0;
}

.waiting-row-destination {
  flex: 1;
  font-family: inherit;
  font-size: 0.82rem;
  padding: 6px 10px;
  border-radius: 999px;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  color: var(--color-navy);
}

.waiting-row-accept,
.waiting-row-reject {
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--card-border);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.waiting-row-accept {
  color: #ffffff;
  background: #2e7d32;
  border-color: #2e7d32;
}

.waiting-row-reject {
  color: var(--color-error, #c62828);
  background: var(--card-bg);
}

.waiting-row-accept:disabled,
.waiting-row-reject:disabled {
  opacity: 0.45;
  cursor: default;
}

/* پروژه‌ها (2026-08-18). MVP list -- same .item-row/.item-row-content
   glass recipe as every other list surface (no swipe gesture here: a tap
   anywhere on the row opens it for editing, same as a waiting row's
   context rather than a home/calendar/tasks commitment). */
.projects-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 10px;
}

.projects-toolbar .submit-button {
  font-size: 0.9rem;
  padding: 9px 16px;
  min-height: 0;
}

.project-row-content {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  cursor: pointer;
  /* --item-accent set per-row in app.js (buildProjectRow), same
     mechanism .cal-dot/.item-row already use -- a 3px right border in the
     project's own color, the simplest "which project is this" cue. */
  border-right: 3px solid var(--item-accent, var(--color-navy));
}

.project-row-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.project-status-pill {
  font-size: 0.78rem;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--color-field-fill);
  color: var(--color-navy);
}

.project-status-pill--done {
  color: #2e7d32;
}

.project-status-pill--abandoned {
  color: var(--color-text-muted);
}

.project-row-note {
  font-size: 0.82rem;
  color: var(--color-text-muted);
}

/* Goal + creation/update stamps on the LIST row (2026-08-18 explicit
   report: a project with no status_note showed literally nothing but its
   name and a status pill -- "no info on these projects"). */
.project-row-goal {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}

.project-row-dates {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  opacity: 0.8;
}

/* Archived (غیرفعال) project row (2026-08-18 bug fix): dimmed so it reads
   as inactive at a glance, with an inline restore action right there --
   there used to be no way back once a project was archived. */
.project-row--archived .project-row-content {
  opacity: 0.6;
}

.project-row-restore {
  align-self: flex-start;
  opacity: 1;
}

.project-edit-archive {
  margin-top: 4px;
  width: 100%;
  justify-content: center;
  color: var(--color-error, #c62828);
}

/* پروژه detail view (2026-08-18, second pass) -- everything else in that
   view reuses Archive's own .archive-section/.archive-note-*/.archive-
   image-*/.archive-file-*/.archive-add-button classes directly (plain
   class selectors, not scoped to #archive-detail-view), so this is the
   one genuinely new rule needed: هدف shown under the status line, same
   weight as .archive-detail-meta but on its own line since it can run
   longer than a status/note pair. */
.project-detail-goal {
  margin: 0 2px 4px;
  font-size: 0.88rem;
  color: var(--color-text-muted);
  line-height: 1.5;
}

/* Creation/last-update stamps (2026-08-18 explicit request) -- smaller and
   lighter than the goal line, since it's metadata to glance at, not the
   project's own content. */
.project-detail-dates {
  margin: 0 2px 6px;
  font-size: 0.78rem;
  color: var(--color-text-muted);
  opacity: 0.85;
}

.project-detail-dates:empty {
  display: none;
}

/* --- Floating record button (bottom-left, 44px minimum tap target) --- */

/* Wrapper (2026-08-16): the fixed positioning that used to live on
   .record-fab itself moved here, so .record-cancel-fab -- an ordinary
   position:absolute child -- can sit pinned to this same fixed spot on
   the viewport without needing its own fixed coordinates. */
.record-fab-wrap {
  position: fixed;
  left: 16px;
  bottom: 76px;
  width: 44px;
  height: 44px;
  z-index: 20;
}

.record-fab {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(20, 64, 107, 0.68);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  border: 1px solid rgba(255, 255, 255, 0.3);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.3), inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 8px 18px -6px rgba(20, 64, 107, 0.45);
  -webkit-appearance: none;
  appearance: none;
}

.record-fab svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
}

.record-fab.is-recording {
  background: var(--color-error);
}

.record-fab:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Cancel × (2026-08-16, explicit request): small badge pinned to the
   record button's top-outer corner, only shown mid-recording (toggled in
   app.js alongside .is-recording, not via a CSS sibling selector, since
   its own hidden attribute needs to flip in step with the recorder's
   actual state, not just the button's class). Deliberately small (20px)
   and off to the side so it can't be mistaken for the main record/stop
   tap target. */
.record-cancel-fab {
  position: absolute;
  top: -6px;
  inset-inline-end: -6px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.6);
  border-radius: 50%;
  background: var(--color-error, #c62828);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 2px 6px -1px rgba(20, 64, 107, 0.45);
  -webkit-appearance: none;
  appearance: none;
  z-index: 21;
}

.record-cancel-fab svg {
  width: 12px;
  height: 12px;
  fill: currentColor;
}

/* Not shown all the time (2026-08-16 correction): app.js already toggles
   the `hidden` attribute in step with mediaRecorder's real state, but the
   unconditional `display: flex` above this in the cascade was winning
   over the UA stylesheet's own `[hidden] { display: none }` rule -- this
   is what actually made the × read as always-visible. */
.record-cancel-fab[hidden] {
  display: none;
}

/* iOS-style wheel date/time picker (2026-08-16, replaces the capture
   sheet's old text-field-opens-a-big-modal flow -- see
   #capture-manual-date-row in home.html and buildWheelColumn() in
   app.js). Every column is its own independently-scrollable, scroll-snap
   list; ROW_H below (32px) must match the JS constant of the same name,
   since app.js computes the settled index from raw scrollTop / ROW_H. */
.wheel-picker {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: var(--color-field-fill);
  border-radius: 12px;
  padding: 0 4px;
}

.wheel-picker-highlight {
  position: absolute;
  left: 8px;
  right: 8px;
  top: 50%;
  height: 32px;
  transform: translateY(-50%);
  border-top: 1px solid var(--card-border);
  border-bottom: 1px solid var(--card-border);
  background: var(--card-bg-strong);
  border-radius: 6px;
  pointer-events: none;
  z-index: 0;
}

.wheel-picker-col {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
  height: 160px;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  padding: 64px 0;
  /* Fades the top/bottom couple of rows instead of hard-clipping them --
     this is most of what reads as "wheel" rather than "scrollable list". */
  mask-image: linear-gradient(180deg, transparent, #000 30%, #000 70%, transparent);
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 30%, #000 70%, transparent);
  scrollbar-width: none;
}

.wheel-picker-col::-webkit-scrollbar {
  display: none;
}

.wheel-picker-col--narrow {
  flex: 0 0 38px;
}

/* Month names ("فروردین"/"اردیبهشت") need real room -- day and year are
   both short numbers and share the default flex: 1. */
.wheel-picker-col--wide {
  flex: 1.8;
}

.wheel-picker-item {
  height: 32px;
  line-height: 32px;
  text-align: center;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-text-muted);
  scroll-snap-align: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  padding: 0 2px;
}

/* Month names ("اردیبهشت") are the longest labels in any wheel -- shrink
   just enough to avoid ellipsis-truncating them under normal font
   scaling, without shrinking every other (short, numeric) column too. */
.wheel-picker-col--wide .wheel-picker-item {
  font-size: 0.82rem;
}

.wheel-picker-item.is-selected {
  color: var(--color-navy);
}

/* Wheel load-failure message (2026-08-21): replaces the day column when
   the wheel's data fetch fails after a retry -- see
   createWheelDatePicker's renderInitError() -- so a broken load reads as
   "tap to retry", not a silent blank box. */
.wheel-picker-error {
  padding: 10px 8px;
  font-size: 0.78rem;
  line-height: 1.4;
  text-align: center;
  color: var(--color-text-muted);
  cursor: pointer;
  white-space: normal;
}

/* Hour/minute are plain numbers, not Persian words -- same LTR-isolation
   already used for item-row-time elsewhere, so "12" renders as one
   ordered value instead of digit-order following the surrounding RTL
   flow. */
.wheel-picker-col--narrow .wheel-picker-item {
  direction: ltr;
  unicode-bidi: isolate;
}

.wheel-picker-sep {
  position: relative;
  z-index: 1;
  font-weight: 700;
  color: var(--color-navy);
}

/* --- Bottom nav (icons only) --- */

.bottom-nav {
  /* justify-content was space-around (2026-08-14 fix: real bug --
     with only 4 icons today (Home/Calendar/Tasks/More), space-around
     stretched them edge-to-edge across the full bar width, reading as
     wasted space rather than a toolbar sized for its actual icon count.
     Centering the cluster with an explicit, fixed gap between icons
     keeps their spacing visually consistent regardless of count, and
     still has real room to grow toward 7 icons later without another
     layout change -- the cluster just gets wider, gaps don't change. */
  position: fixed;
  left: 0;
  right: 0;
  /* Bottom overscan (2026-08-15, ninth pass): confirmed via a real iPhone
     screenshot (not Chrome, which sat flush with no gap) that page
     background was visible below this bar -- the fixed box wasn't
     actually reaching the true bottom edge, the same class of viewport-
     timing gap already fixed for the TOP edge (.app's negative
     margin-top overscan, 2026-08-15 third pass) but never applied down
     here. Same fix, mirrored: push the box's bottom edge one
     safe-area-inset-bottom PAST the nominal viewport bottom (negative
     `bottom`), and grow height by that same extra inset so the box's top
     edge stays exactly where it was. The bit that renders past the true
     screen edge is simply clipped by the device -- harmless -- but
     guarantees the visible bar can never fall short regardless of
     Safari's chrome-hide/show timing. padding-bottom below is doubled to
     match (same 2x-inset pattern as the top fix) so .nav-item's actual
     position, anchored to the bottom of the content box via
     align-items: flex-end, ends up unchanged -- this only extends the
     invisible safety margin, not where the icons themselves sit. */
  bottom: calc(-1 * env(safe-area-inset-bottom, 0px));
  /* Real root cause, 2026-08-14, found from a precise repro report ("the
     cursor bug only appears if I touch slightly BELOW the icon, not the
     icon itself" -- confirmed live to still exist after every earlier CSS
     fix, all of which only ever touched .nav-item/svg/.bottom-nav's own
     touch-action/user-select/tap-highlight, never this): this element was
     never extended to cover env(safe-area-inset-bottom) -- the home-
     indicator strip on any notch/home-button-less iPhone. bottom:0 makes
     the BOX start at the true screen edge, but with a fixed 64px height
     and no extra bottom padding, the box's PROTECTED area (touch-action,
     user-select, tap-highlight) only extends 64px up from the edge --
     while the safe-area inset itself is typically ~34px, meaning the
     lowest slice of this "toolbar" was never actually inside a
     touch-action:manipulation/user-select:none box at all. A touch
     landing there fell through to native iOS text-selection handling on
     whatever's rendered underneath (the scrollable list, unprotected).
     grep across this whole file (2026-08-14) confirmed env(safe-area-
     inset-bottom) was used in exactly one place (.sheet) and nowhere
     near this nav -- so every earlier fix here was real but incomplete,
     addressing the icons themselves while leaving a strip below them
     structurally unprotected regardless of which touch-action/user-select
     rules got added to .nav-item.
     Fix: extend the box (via padding, box-sizing:border-box so the
     ORIGINAL 64px stays the icons' own centered content area, unchanged
     visually) down through the full safe-area inset, so the touch-action/
     user-select/tap-highlight rules below now genuinely cover the whole
     physical strip down to the true device edge -- nothing left exposed
     for a mistouch to fall through to. */
  box-sizing: border-box;
  /* Sixth pass (2026-08-15, explicit correction): the fifth pass dropped
     backdrop-filter to test whether blur itself (not its radius) was
     causing the "thick" read -- wrong call, per direct feedback: removing
     the blur made scrolled content behind the bar show through clearly,
     which reads as a bug, not a thinner bar. Blur is restored (back to
     the fourth-pass 6px/130% values, background/border opacity restored
     to match) and left alone from here on. The actual lever for "thinner"
     is the bar's own height, which this pass cuts for real: 56px -> 48px.
     (.nav-item's 44px min-height still fits inside 48px with room to
     spare; .app's matching padding-bottom below is updated to 48px too so
     scrollable content still clears the bar exactly.) */
  /* Seventh pass (2026-08-15, explicit request: "half the size, or a
     little more than half"): 48px -> 28px. This drops .nav-item below
     Apple's 44pt tap-target guideline (see .nav-item below) -- accepted
     trade-off for a personal single-user app, not an App-Store-facing
     product. If icons feel cramped to tap in practice, the fix is a
     slightly taller bar again, not smaller icons. */
  /* Height grown by one extra safe-area-inset-bottom (ninth pass) to pair
     with the negative `bottom` above -- see that comment for the full
     reasoning and the worked example keeping icon position unchanged. */
  height: calc(28px + (2 * env(safe-area-inset-bottom, 0px)));
  padding-bottom: calc(2 * env(safe-area-inset-bottom, 0px));
  display: flex;
  /* flex-end, not center (2026-08-15, explicit request: "do not change
     icon size, move icons lower"): .nav-item's own vertical padding was
     removed below so its box is now exactly the 20px icon, with no slack
     of its own -- flex-end anchors that icon flush to the bottom of this
     28px content box (i.e. just above the safe-area padding below),
     leaving the ~8px of now-unused vertical room as blank space above the
     icons instead of centred padding around them. Icon size itself
     (.nav-item svg, 20px) is untouched. */
  align-items: flex-end;
  justify-content: center;
  gap: 12px;
  background: rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(6px) saturate(130%);
  -webkit-backdrop-filter: blur(6px) saturate(130%);
  border-top: 1px solid rgba(255, 255, 255, 0.4);
  z-index: 10;
  /* Real bug, 2026-08-14: the same touch-action/user-select rules were
     already added to .nav-item, but the report persisted -- most likely
     explanation, confirmed separately: the fix was never actually
     visible live at all (static_version was computed once at process
     start, so the CSS URL never changed without a restart this
     environment cannot perform -- see static_version's own comment in
     app.py, fixed the same night). Adding the same protection here too,
     on the parent <nav>, as defense in depth regardless -- some mobile
     browsers resolve a long-press selection gesture by walking up to the
     nearest ancestor that allows it, so belt-and-suspenders costs
     nothing and removes any doubt about which exact element needed it. */
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.nav-item {
  /* Horizontal padding reduced from 18px (2026-08-14, paired with the
     .bottom-nav gap change above): 18px per side was sized for
     space-around stretching 4 icons across the full bar -- with a fixed
     gap doing that job now, the same padding on top of it re-created the
     same "too spread out" look one level down.
     min-height/padding cut again (seventh pass, 2026-08-15) to fit the
     28px bar: 44px -> 28px, matching .bottom-nav's new height exactly (20px
     icon + 4px vertical padding = 28px). Below Apple's 44pt tap-target
     guideline -- explicit trade-off, see .bottom-nav's height comment.
     min-width kept wider than min-height on purpose: horizontal room
     costs nothing (icons already have a fixed 12px gap between them) and
     a slightly wider horizontal tap zone helps offset the shorter
     vertical one. */
  border: none;
  background: none;
  /* Vertical padding dropped to 0 (was 4px, eighth pass, 2026-08-15):
     freed-up room is what lets .bottom-nav's flex-end anchor the icon
     lower instead of centred -- see .bottom-nav's align-items comment.
     Icon itself (svg below) is still 20px, untouched. Horizontal padding
     kept for tap-width and gives the touch target more room than the
     visible icon alone. */
  padding: 0 14px;
  min-height: 20px;
  min-width: 40px;
  cursor: pointer;
  /* Brighter/lighter than --color-text-muted (2026-08-15, explicit
     request: "selected icon slightly darker and others slightly
     brighter, showing that icon is selected") -- a dedicated lighter
     tone here rather than reusing the shared muted-text variable
     (which stays as-is for actual text elsewhere), so the gap between
     this and .nav-item.is-active reads more clearly at a glance. */
  color: #94a8bc;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-appearance: none;
  appearance: none;
  /* Real bug, 2026-08-14: tapping a toolbar icon could trigger iOS's
     native press-and-hold text-selection/callout UI at the bottom of the
     screen -- no rule anywhere in this file told the browser these
     buttons aren't selectable text, so any touch registering with
     slightly more hold time than a clean tap (common on a real device,
     more so with 6 icons now packed more tightly than the previous 4)
     fell through to that default OS gesture. touch-action: manipulation
     also removes the double-tap-to-zoom delay/ambiguity window that
     makes a quick tap more likely to be misread as a hold in the first
     place. */
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  /* Real gap found 2026-08-14 re-investigating the still-open "horizontal
     cursor/selection UI on toolbar tap" report from a fresh look, not a
     repeat of the touch-action/user-select rules above (those were
     already here and the bug persisted): -webkit-tap-highlight-color was
     never set ANYWHERE in this file. iOS Safari's default tap highlight
     is a translucent overlay drawn over the tapped element's full box on
     every touch -- and .nav-item has no border-radius, so that overlay
     renders as a sharp-edged rectangle across the icon's 44px padding
     box, which could easily read as "a horizontal bar" across a toolbar
     of tightly-packed icons. transparent removes the overlay entirely;
     the existing .nav-item.is-active color change is the only tap
     feedback needed. */
  -webkit-tap-highlight-color: transparent;
}

.nav-item svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
  /* SVG has its own separate selection/pointer-event model in some
     engines -- user-select/touch-action on the parent button isn't
     guaranteed to be inherited down to it, so stated explicitly here too. */
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}

.nav-item.is-active {
  /* Slightly darker than --color-navy (2026-08-15, explicit request),
     paired with the lighter unselected colour above so the selected
     icon is pulled apart from the rest in both directions at once. */
  color: #0f3252;
}

/* --- Capture sheet (slides up over Home after a recording) --- */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(10, 39, 64, 0.35);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 30;
}

.sheet-backdrop[hidden] {
  /* Author `display: flex` above would otherwise beat the UA stylesheet's
     implicit [hidden] rule -- the exact cascade bug found on 2026-07-29
     with .proposal-item[hidden]. */
  display: none;
}

.sheet {
  position: relative;
  width: 100%;
  max-width: 480px;
  background: #ffffff;
  border-radius: 16px 16px 0 0;
  padding: 20px 18px calc(18px + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
  gap: 12px;
  text-align: right;
  box-shadow: 0 -6px 24px rgba(20, 64, 107, 0.25);
  animation: sheet-slide-up 0.28s cubic-bezier(0.22, 0.61, 0.36, 1) both;
  /* Fixed 2026-09-03 (explicit report: "in در انتظار tab swiping
     just moves the background, I can't see all the messages"). The sheet had
     no height cap and no scroller, so a long list grew past the viewport with
     nothing scrollable -- and .sheet-backdrop is position:fixed/inset:0, so
     the page behind couldn't scroll either. dvh (with a vh fallback for older
     browsers) keeps the cap correct while mobile browser chrome collapses. */
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

@keyframes sheet-slide-up {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

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

.sheet-close {
  position: absolute;
  top: 12px;
  left: 14px;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: #eef4fa;
  color: var(--color-navy);
  font-size: 1.1rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.sheet-heading {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-text-muted);
}

.sheet-transcript {
  margin: 0;
  font-size: 0.98rem;
  line-height: 1.6;
  color: var(--color-text);
  white-space: pre-wrap;
}

.sheet-note {
  margin: 0;
  font-size: 0.85rem;
  color: var(--color-error);
}

.sheet-reminder-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.88rem;
  color: var(--color-text);
}

.sheet-note[hidden],
.sheet-reminder-toggle[hidden],
.sheet .item-row[hidden] {
  display: none;
}

/* --- Archive ("آرشیو") --- */
/* A full page like Calendar/Tasks (see [data-panel="archive"]), not a
   modal -- so it needs its own small header with a back control instead
   of the sheet-close pattern the popup version used. */

.archive-page-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.archive-page-title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--color-navy);
}

.archive-lock-note {
  color: var(--color-text-muted);
}

.archive-lock-note--error {
  color: var(--color-error);
}

.archive-search-row input[type="search"] {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 12px 14px;
  color: var(--color-text);
  outline: none;
}

.archive-search-row input[type="search"]:focus {
  box-shadow: 0 0 0 2px var(--color-navy);
}

.archive-pin-row {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.archive-pin-row[hidden] {
  display: none;
}

.archive-pin-row input[type="password"] {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 1.1rem;
  letter-spacing: 0.2em;
  direction: ltr;
  text-align: center;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 10px 12px;
  color: var(--color-text);
  outline: none;
}

.archive-pin-row input[type="password"]:focus {
  box-shadow: 0 0 0 2px var(--color-navy);
}

.archive-pin-row .submit-button {
  flex: none;
  padding: 0 18px;
}

.quick-add-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 12px;
}

.quick-add-row input[type="text"] {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.95rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 10px 12px;
  color: var(--color-text);
  outline: none;
}

.quick-add-row input[type="text"]:focus {
  box-shadow: 0 0 0 2px var(--color-navy);
}

.quick-add-row input[type="time"] {
  flex: none;
  width: 110px;
  font-family: inherit;
  font-size: 0.85rem;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 9px 10px;
  color: var(--color-text);
  outline: none;
}

.quick-add-row input[type="time"][hidden] {
  display: none;
}

.quick-add-time-toggle,
.quick-add-submit {
  flex: none;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  border: none;
  background: var(--color-field-fill);
  color: var(--color-navy);
  cursor: pointer;
  -webkit-appearance: none;
  padding: 0;
}

.quick-add-time-toggle svg,
.quick-add-submit svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

.quick-add-time-toggle.is-active {
  background: var(--color-navy);
  color: #ffffff;
}

.quick-add-submit {
  background: var(--color-navy);
  color: #ffffff;
}

.quick-add-error {
  margin: -6px 0 10px;
  font-size: 0.78rem;
  color: var(--color-error);
}

/* Quick-add FAB (2026-08-15 redesign): replaced the old inline +/clock
   row -- a single button that opens the shared item-edit-sheet in create
   mode instead. */
.quick-add-fab {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin: 4px 0 12px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  background: rgba(20, 64, 107, 0.68);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.3), inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 6px 14px -6px rgba(20, 64, 107, 0.4);
  color: #ffffff;
  cursor: pointer;
  -webkit-appearance: none;
  padding: 0;
}

.quick-add-fab svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
}

/* Priority color picker (2026-08-15): four swatches + "no color", used in
   both the item-edit sheet and the voice-confirm sheet. Deliberately NOT
   the same element as the existing item-select-circle dot -- this is an
   input control (a radio group of buttons), that one is a read-only
   indicator + checkbox. Kept visually similar (same size family) on
   purpose so the color chosen here reads as "this is what that dot will
   look like". */
.color-picker {
  display: flex;
  align-items: center;
  gap: 10px;
}

.color-swatch {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  -webkit-appearance: none;
  padding: 0;
}

.color-swatch--none {
  background: #ffffff;
  border-color: var(--color-field-fill);
}

.color-swatch--red { background: #c62828; }
.color-swatch--yellow { background: #f9a825; }
/* Real white (2026-08-15), distinct from --none: --none is the "no color
   chosen" option and stays visually muted (pale fill, light border) so it
   never reads as an actual color choice; --white is a real, selectable
   priority color and gets a solid border so it doesn't just look like an
   empty circle. */
.color-swatch--white { background: #ffffff; border-color: var(--color-text-muted); }
.color-swatch--blue { background: #1565c0; }
.color-swatch--green { background: #2e7d32; }

.color-swatch.is-selected {
  border-color: var(--color-navy);
  box-shadow: 0 0 0 2px #ffffff inset;
}

.archive-new-row {
  display: flex;
  gap: 8px;
  margin: 10px 0;
}

.archive-new-row input[type="text"] {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.95rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 10px 12px;
  color: var(--color-text);
  outline: none;
}

.archive-new-row input[type="text"]:focus {
  box-shadow: 0 0 0 2px var(--color-navy);
}

.archive-new-button {
  flex: none;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0 16px;
  border-radius: 10px;
  border: none;
  background: var(--color-navy);
  color: #ffffff;
  cursor: pointer;
  /* Same stronger depth recipe as .submit-button (2026-08-16, second
     pass -- see its own comment for why --card-shadow alone wasn't
     enough on a small solid button). */
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.35),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 10px 18px -8px rgba(20, 64, 107, 0.45);
  -webkit-appearance: none;
  appearance: none;
}

.archive-new-button:disabled {
  opacity: 0.5;
  cursor: default;
}

.archive-row-title-wrap {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.archive-search-snippet {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.archive-search-highlight {
  background: var(--color-field-fill);
  color: var(--color-navy);
  font-weight: 600;
  border-radius: 3px;
  padding: 0 1px;
}

.archive-detail-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

.archive-detail-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: var(--color-text);
  flex: 1;
  min-width: 0;
}

.archive-detail-title-edit {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

.archive-detail-title-edit[hidden] {
  display: none;
}

.archive-detail-title-edit input[type="text"] {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.95rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 8px;
  padding: 8px 10px;
  color: var(--color-text);
  outline: none;
}

.archive-detail-title-edit .bulk-action-button {
  flex: none;
  padding: 8px 14px;
}

.archive-detail-meta {
  margin: 8px 0;
  font-size: 0.82rem;
  color: var(--color-text-muted);
}

/* Small شخصی/کاری badge on each archive row. The row's own
   .item-select-circle accent (via wrapRowForSwipe's sourceClass in
   app.js) reuses the same blue=کاری/green=شخصی mapping as this badge, for
   visual consistency between the two -- the badge's text label is still
   the primary way category is communicated, since color alone isn't. */
.archive-tag {
  flex: none;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
}

.archive-tag--personal {
  color: #2e7d32;
  background: rgba(46, 125, 50, 0.12);
}

.archive-tag--work {
  color: #1565c0;
  background: rgba(21, 101, 192, 0.12);
}

/* --- Item edit sheet --- */

.item-row-content {
  cursor: pointer;
}

.item-row-content:active {
  opacity: 0.8;
}

/* --- Calendar grid (Jalali month view) --- */

.cal-week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

/* Weekday header, week view only (added 2026-08-15) -- same 7-column grid
   as .cal-week so شنبه..جمعه line up with the day cells directly below. */
.cal-weekday-header {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  margin-bottom: 6px;
}

.cal-weekday-header span {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-align: center;
}

.cal-cell {
  /* aspect-ratio: 1 (removed 2026-08-14): harmless for month view, where
     each cell is already narrow (1/7 of the row), so forcing height to
     match that narrow width kept cells roughly square. Day view fills the
     row with a single cell (see app.js's per-week gridTemplateColumns),
     so the same rule forced its height to match the FULL row width --
     rendering the single day as a giant square nearly as large as the
     whole month grid. background/border/border-radius/padding below are
     already overridden by the second .cal-cell block further down this
     file (confirmed via cascade: same property there wins); this rule's
     remaining effective properties are display/flex-direction/
     align-items/justify-content/position, which that later block never
     redeclares and still relies on. */
  background: #fafafa;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  position: relative;
}

.cal-cell--other-month {
  /* Strengthened 2026-08-14: real bug, confirmed with real evidence --
     the grid's leading cells (previous month's tail, shown so weekday
     columns align, e.g. Mordad 1st landing mid-week means Tir 27-31 fill
     the cells before it) read as "the month starts from 27" instead of
     muted padding, because the only distinction was #f0f0f0 vs the
     regular cell's #f7fafd background -- a ~7-10/255 difference per
     channel, too subtle to register as "this isn't really part of the
     month" at a glance. A dedicated, stronger opacity-based rule already
     existed in this file (.cal-cell--outside) but was dead CSS: JS has
     never emitted that class name, only cal-cell--other-month. Merged the
     two into the one that's actually applied. */
  color: var(--color-text-muted);
  opacity: 0.45;
}

.cal-cell--today {
  border: 2px solid var(--color-navy);
}

.cal-day {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--color-navy);
  text-align: center;
}

.cal-items {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 2px;
  width: 100%;
  flex: 1;
  overflow: hidden;
}

.cal-item {
  font-size: 0.7rem;
  padding: 2px 4px;
  border-radius: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  text-align: center;
}

.cal-item--calendar {
  background: #bbdefb;
  color: #0d47a1;
}

.cal-item--task {
  background: #c8e6c9;
  color: #1b5e20;
}

.cal-item:hover {
  opacity: 0.9;
}

/* Calendar dots (visual indicators) */
.cal-dots {
  display: flex;
  gap: 3px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 2px;
}

.cal-dot {
  box-sizing: border-box;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  /* Same visibility fix as .item-select-circle-input (2026-08-15): a
     white-priority dot needs a border to not disappear against the page. */
  border: 1px solid var(--color-field-fill);
  display: inline-block;
}

.cal-dot--calendar {
  background: #1565c0;
}

.cal-dot--task {
  background: #2e7d32;
}

.day-items-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 60vh;
  overflow-y: auto;
}

/* --- "More" drawer rows --- */

.more-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.more-row {
  font-family: inherit;
  font-size: 0.98rem;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 6px;
  border: none;
  background: none;
  color: var(--color-text);
  text-align: right;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.more-row svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
  flex: none;
}

.more-row:disabled {
  /* Dimmed rather than hidden: these are real, planned destinations, so
     showing them greyed communicates "not yet" instead of "doesn't exist". */
  color: var(--color-text-muted);
  opacity: 0.5;
  cursor: not-allowed;
}

.more-row-note {
  margin-inline-start: auto;
  font-size: 0.78rem;
}

.more-settings-panel {
  border-top: 1px solid #e6eef6;
  padding-top: 12px;
}

.more-settings-panel[hidden] {
  display: none;
}

.more-logout {
  display: inline-block;
  color: var(--color-navy);
  font-size: 0.95rem;
  font-weight: 600;
}

/* --- Tab panels --- */

.panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex: 1;
}

.panel[hidden] {
  /* Author `display: flex` above beats the UA [hidden] rule -- same cascade
     trap as .sheet-backdrop and .proposal-item before it. */
  display: none;
}

.panel-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: var(--color-navy);
}

/* --- Task rows (selection circle + time + label) --- */

.item-row-edit {
  flex: none;
  border: none;
  background: none;
  padding: 4px;
  cursor: pointer;
  color: var(--color-text-muted);
  -webkit-appearance: none;
  appearance: none;
}

.item-row-edit svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
  display: block;
}

.item-row-date {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  flex: none;
}

/* --- Calendar tab (Jalali grid) --- */

.cal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.cal-title {
  font-weight: 700;
  font-size: 1rem;
  color: var(--color-navy);
}

.cal-nav {
  width: 30px;
  height: 30px;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  background: var(--card-bg);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow);
  color: var(--color-navy);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* #cal-grid's actual children (from renderCalendar() in app.js) are
   .cal-week wrapper divs, one per week -- NOT a flat list of day cells.
   This container must therefore stack weeks vertically; the 7-column grid
   belongs on .cal-week itself (see the "Calendar grid (Jalali month view)"
   block above), which already provides it. A 7-column grid was previously
   applied here too, which forced the (at most 6) .cal-week divs into a
   single horizontal row instead of one row per week -- confirmed live via
   a same-origin reproduction against the deployed CSS, 2026-08-11. */
.cal-grid {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.cal-weekday {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-align: center;
  padding-bottom: 2px;
}

.cal-cell {
  position: relative;
  min-height: 40px;
  padding: 4px 2px 12px;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  background: var(--card-bg);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  /* -deep, not -thin (2026-08-15, "more depth" request) -- see the token's
     own comment for why this isn't the earlier inset-pair approach that
     was reverted on this exact surface. */
  box-shadow: var(--card-shadow-deep);
  color: var(--color-text);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}


.cal-cell--today {
  /* Border, not outline (2026-08-15, third pass): this used to add a 2px
     outline pulled 2px inward on TOP OF .cal-cell's own 1px border --
     literally two concentric rings around today's cell. That's the other
     real source of the "double line" report (the first fix only touched
     card-shadow, never this). Replacing the outline with the cell's own
     border removes the second ring; still reads as clearly highlighted
     via the navy border + tinted background. */
  border: 2px solid var(--color-navy);
  font-weight: 700;
  background: rgba(191, 227, 250, 0.7);
}

.cal-cell--selected {
  background: rgba(191, 227, 250, 0.75);
}

/* Per-source dots, reusing the same --item-accent variable the list rows
   use, so a new source needs no new calendar-specific rule. */
.cal-dots {
  position: absolute;
  bottom: 4px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 3px;
}

.cal-dot {
  box-sizing: border-box;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  border: 1px solid var(--color-field-fill);
  background: var(--item-accent, var(--color-text-muted));
  /* display + flex:none matter when this dot is reused standalone in a
     list row (cal-list, below the grid) rather than inside the absolutely-
     positioned .cal-dots grid-cell container: a bare <span> is inline by
     default, which ignores width/height entirely. */
  display: inline-block;
  flex: none;
}

/* Project-linked month-view dot (2026-08-18, updated same-day follow-up):
   a thin ring, layered on top of the dot's normal priority-color fill via
   box-shadow (an actual wider border would change the dot's box size
   inside the tight month-grid cell). Fixed color now, not per-project --
   project color itself was removed in the follow-up request. */
.cal-dot--project {
  box-shadow: 0 0 0 1.5px var(--color-text-muted);
}

.cal-list-wrap {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Without this, the `hidden` attribute (set via JS: CAL_LIST_WRAP.hidden =
   true) is overridden by the `display: flex` rule above, since a class
   selector beats the browser's built-in `[hidden] { display: none }` UA
   rule. This silently kept the calendar list visible in day view even
   when JS correctly marked it hidden. Confirmed live 2026-08-14. */
.cal-list-wrap[hidden] {
  display: none;
}

.cal-day-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* --- Archive subject notes/images/files (2026-08-14) --- */

.archive-section {
  margin-top: 16px;
}

.archive-section[hidden] {
  display: none;
}

.archive-section-label {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin-bottom: 8px;
}

.archive-note-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  /* Given real depth for the first time (2026-08-15, "more depth" request)
     -- this row was flat (solid #f7fbfe fill, plain border, no shadow at
     all) before, unlike .item-row-content/.cal-cell which already had a
     restrained shadow. Nothing here sits on a coloured layer underneath
     it the way swipe-to-delete rows do, so unlike that row this one can
     safely take the full glass treatment (translucent fill + blur), not
     just a shadow bolted onto the old flat background. */
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  backdrop-filter: blur(16px) saturate(190%);
  -webkit-backdrop-filter: blur(16px) saturate(190%);
  box-shadow: var(--card-shadow-deep);
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 8px;
}

.archive-note-row-body {
  flex: 1;
  min-width: 0;
}

.archive-note-row-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
}

.archive-note-row-date {
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 4px;
}

.archive-note-row-text {
  font-size: 0.88rem;
  color: var(--color-text);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.archive-note-row-text textarea {
  width: 100%;
  font-family: inherit;
  font-size: 0.88rem;
  direction: rtl;
  text-align: right;
  background: #ffffff;
  border: 1px solid var(--color-field-fill);
  border-radius: 8px;
  padding: 8px;
  color: var(--color-text);
  outline: none;
  resize: vertical;
}

.archive-note-row-edit-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

.archive-note-compose {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 8px;
}

.archive-note-compose[hidden] {
  display: none;
}

.archive-note-compose textarea {
  width: 100%;
  font-family: inherit;
  font-size: 0.9rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 10px;
  padding: 10px 12px;
  color: var(--color-text);
  outline: none;
  resize: vertical;
}

.archive-note-compose-actions {
  display: flex;
  gap: 8px;
}

.archive-image-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.archive-image-thumb {
  position: relative;
  border-radius: 10px;
  overflow: hidden;
  aspect-ratio: 1;
  border: none;
  padding: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: var(--color-field-fill);
}

.archive-image-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Hidden until the browser's own `load` event fires (2026-08-16) -- see
   renderArchiveImageThumb() in app.js. Not display:none: this keeps the
   image's layout box (and this button's aspect-ratio-driven size)
   reserved the whole time, so nothing shifts when it appears. */
.archive-image-thumb img.is-loading {
  visibility: hidden;
}

/* Upload-in-progress placeholder card (2026-08-16): same grid slot/size
   as a real thumb (reuses .archive-image-thumb itself), holding a
   progress bar + live "uploaded / total" label instead of an image --
   see uploadArchiveAttachment() in app.js. */
.archive-image-thumb--uploading {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 8px;
  padding: 12px;
  cursor: default;
}

.archive-upload-progress-bar {
  height: 6px;
  border-radius: 999px;
  background: rgba(20, 64, 107, 0.12);
  overflow: hidden;
}

.archive-upload-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--color-navy);
  transition: width 0.15s ease-out;
}

.archive-image-thumb--uploading .archive-image-thumb-name {
  position: static;
  background: none;
  color: var(--color-text-muted);
  padding: 0;
  text-align: center;
  direction: ltr;
  unicode-bidi: isolate;
}

.archive-image-thumb-name {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 4px 6px;
  background: rgba(20, 64, 107, 0.55);
  color: #ffffff;
  font-size: 0.68rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Corner overlay icons (2026-08-17, explicit request): delete top-left,
   download top-right -- act directly on the thumbnail without opening
   the full-image sheet first. Round, semi-opaque dark circles so they
   read against any photo underneath, same idea as the record-cancel FAB
   elsewhere in this app. z-index above .archive-image-thumb-name/img
   (both default stacking, no z-index of their own). */
.archive-image-thumb-delete,
.archive-image-thumb-download {
  position: absolute;
  top: 6px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 39, 64, 0.55);
  color: #ffffff;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  z-index: 2;
}

.archive-image-thumb-delete svg,
.archive-image-thumb-download svg {
  width: 14px;
  height: 14px;
  fill: currentColor;
}

.archive-image-thumb-delete {
  left: 6px;
}

.archive-image-thumb-download {
  right: 6px;
}

.archive-file-row {
  display: flex;
  align-items: center;
  gap: 6px;
  background: #f7fbfe;
  border: 1px solid var(--color-field-fill);
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 8px;
}

/* Project timeline image row (2026-08-18): a compact thumbnail + filename
   row, reusing .archive-file-row's card but with a real image instead of
   the file glyph -- needed because the combined chronological timeline
   can't use the old 2-column image grid (that only works when every
   image is grouped together, not interleaved with tasks/text). */
.project-timeline-image-row {
  cursor: pointer;
}

.project-timeline-image-thumb {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  object-fit: cover;
  flex: none;
  background: var(--color-field-fill);
}

.project-timeline-upload-progress {
  opacity: 0.85;
}

.archive-file-row-link {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  text-decoration: none;
}

.archive-file-row-link svg {
  width: 20px;
  height: 20px;
  fill: var(--color-navy);
  flex: none;
}

.archive-file-row-body {
  flex: 1;
  min-width: 0;
}

.archive-file-row-name {
  font-size: 0.88rem;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.archive-file-row-meta {
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

.archive-detail-toolbar {
  display: flex;
  gap: 8px;
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid var(--color-field-fill);
}

.archive-add-button {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-family: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 9px;
  border-radius: 10px;
  border: 1px solid var(--color-field-fill);
  background: #ffffff;
  color: var(--color-navy);
  cursor: pointer;
  /* Same --card-shadow depth as .submit-button (2026-08-16). */
  box-shadow: var(--card-shadow);
  -webkit-appearance: none;
  appearance: none;
}

.archive-add-button svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
}

.archive-add-button--primary {
  background: var(--color-navy);
  border-color: var(--color-navy);
  color: #ffffff;
  /* Same stronger depth recipe as .submit-button (2026-08-16) -- the
     base .archive-add-button's --card-shadow reads fine on its own white
     background, but was too subtle once this variant switched to a solid
     navy fill. */
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.35),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 10px 18px -8px rgba(20, 64, 107, 0.45);
}

.bulk-action-button--danger {
  color: #ffffff;
  background: var(--color-error);
  border-color: var(--color-error);
}

.archive-image-sheet-img {
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  border-radius: 12px;
  background: var(--color-field-fill);
}

.archive-image-sheet-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.archive-image-sheet-meta .archive-detail-meta {
  margin: 2px 0 0;
}

/* مالی (2026-08-25): capital-allocation calculator sheet, ported from
   the user's Excel تخصیص سرمایه model. Deliberately reuses
   .sheet/.field/.range-pill/.item-row-content/.submit-button exactly as
   the rest of the app -- only new layout/color rules below. */
.financial-sheet {
  max-height: 85vh;
  overflow-y: auto;
}

.financial-settings-grid {
  display: grid;
  /* minmax(0, 1fr), not bare 1fr (2026-08-25 fix): bare 1fr's implicit
     min-width is "auto" -- the widest un-shrinkable child (here, the
     <input>'s own UA-default intrinsic width, since .field input never
     sets width:100% globally) forces each track past its fair half,
     overflowing the sheet by ~20px on a 400px-wide window and cutting
     off the right edge. minmax(0, 1fr) lets tracks shrink to fit; the
     explicit width:100%/min-width:0 below on the input itself removes
     the original overflow source too. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.financial-settings-grid .field {
  min-width: 0;
}

.financial-settings-grid input {
  width: 100%;
  min-width: 0;
}

/* هزینه‌های واردات: قفل سراسری (2026-08-26) -- یک عدد یورویی ثابت که وقتی
   قفل است برای همه‌ی محصولات یکسان استفاده می‌شود (هر کدام با FOB خودش
   درصدش برعکس حل می‌شود)، به‌جای این‌که همیشه درصدی از FOB هر محصول باشد. */
.financial-cost-split {
  display: flex;
  align-items: center;
  gap: 6px;
}

.financial-cost-split .financial-product-lock {
  flex: none;
}

.financial-cost-euro-input {
  width: 100%;
  min-width: 0;
  font-family: inherit;
  font-size: 0.85rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 8px;
  padding: 8px;
  color: var(--color-text);
  outline: none;
}

.financial-cost-euro-input:disabled {
  background: #eef2f5;
  color: var(--color-text-muted);
  cursor: not-allowed;
}

.financial-cost-split #fin-import-cost:disabled {
  background: #eef2f5;
  color: var(--color-text-muted);
  cursor: not-allowed;
}

.financial-derived-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 0.82rem;
  color: var(--color-text-muted);
  background: #eef4fa;
  border-radius: 10px;
  padding: 10px 12px;
}

.financial-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-navy);
}

.financial-add-button {
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 10px;
  background: var(--color-navy);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.35),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 10px 18px -8px rgba(20, 64, 107, 0.45);
  -webkit-appearance: none;
  appearance: none;
}

.financial-add-button svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.financial-product-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.financial-product-row {
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}

.financial-product-row-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.financial-product-name {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  border: none;
  background: transparent;
  color: var(--color-text);
  outline: none;
}

.financial-product-remove {
  flex: none;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: #eef4fa;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.financial-product-lock {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: #eef4fa;
  color: var(--color-text-muted);
  font-size: 0.6rem;
  line-height: 1;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: background 0.15s ease, color 0.15s ease;
}

.financial-product-lock.is-active {
  background: var(--color-navy, #14406b);
  color: #fff;
}

.financial-product-fields {
  display: grid;
  /* minmax(0, 1fr), matching the same overflow fix as
     .financial-settings-grid above (2026-08-25). */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
}

.financial-product-fields > div {
  min-width: 0;
}

.financial-product-fields > div > label {
  display: block;
  font-size: 0.66rem;
  color: var(--color-text-muted);
  margin-bottom: 3px;
}

.financial-product-fields input {
  width: 100%;
  min-width: 0;
  font-family: inherit;
  font-size: 0.85rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 8px;
  padding: 8px;
  color: var(--color-text);
  outline: none;
}

/* Derived/read-only state (2026-08-26): whichever of ضریب فروش / فروش کل
   is NOT the free input right now (driven instead by the other one via
   the lock) is shown disabled in this muted style, rather than being
   swapped out for a different field. */
.financial-product-fields input:disabled,
.financial-result-sale-input:disabled {
  background: #eef2f5;
  color: var(--color-text-muted);
  cursor: not-allowed;
}

.financial-product-result {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  border-top: 1px solid #e6eef6;
  padding-top: 8px;
  font-size: 0.82rem;
  color: var(--color-text-muted);
}

.financial-result-sale {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.financial-result-sale-label {
  flex: none;
  white-space: nowrap;
}

.financial-result-sale-input {
  width: 100%;
  min-width: 0;
  font-family: inherit;
  font-size: 0.82rem;
  direction: rtl;
  text-align: right;
  background: var(--color-field-fill);
  border: none;
  border-radius: 6px;
  padding: 4px 6px;
  color: var(--color-text);
  outline: none;
}

.financial-result-sale-unit {
  flex: none;
}

.financial-product-result b {
  display: block;
  font-size: 0.82rem;
  font-weight: 700;
}

.financial-positive { color: #1f7a34; }
.financial-negative { color: var(--color-error); }

/* Color filter (2026-08-29, explicit request; re-added 2026-08-30 after a
   watchdog restore wiped it out). Toggle button lives in each tab's
   .bulk-action-bar; the popover is one shared instance positioned via JS. */
.color-filter-toggle.is-filtering {
  box-shadow: 0 0 0 2px var(--color-navy);
}

/* Fixed 2026-08-30 (live-verified via Chrome): a plain `.color-filter-popover
   { display: flex }` rule has the same specificity as the browser's own
   `[hidden] { display: none }` UA-stylesheet rule, and author CSS wins
   ties over the UA stylesheet -- so setting the `hidden` attribute in JS
   never actually hid it, which is why the popover couldn't be closed by
   tapping elsewhere. Scoping the flex rule to :not([hidden]), plus an
   explicit higher-specificity [hidden] rule, fixes that. */
.color-filter-popover:not([hidden]) {
  display: flex;
}

.color-filter-popover[hidden] {
  display: none;
}

.color-filter-popover {
  position: fixed;
  flex-direction: row;
  align-items: center;
  gap: 4px;
  padding: 4px;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: 999px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  z-index: 40;
}

/* Shrunk 2026-08-30 (explicit request: "so big, make them smaller") from
   32px/20px down to 24px/14px. */
.color-filter-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  cursor: pointer;
  padding: 0;
  -webkit-appearance: none;
  appearance: none;
}

.color-filter-swatch {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
}

.color-filter-swatch--white {
  background: #ffffff;
  border: 1px solid var(--card-border);
}

.color-filter-swatch--all {
  background: conic-gradient(#c62828, #f9a825, #2e7d32, #1565c0, #c62828);
}
