/* ==========================================================================
   چالش | Design tokens & shared styles
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap');

:root{
  /* ==========================================================================
     THEMES — every colour in this file lives in one of the three blocks below
     and nowhere else. This block is the light theme «شن و مریم‌گلی»
     (sand & sage) and is the base; the two blocks after it restate the same
     token names for the dark theme «شب کرمی» (creamy night), once for a device
     that asks for dark and once for an explicit [data-theme="dark"].
     Nothing downstream may hardcode a colour — a rule that needs a tint uses
     rgba(var(--accent-rgb),a) rather than a second copy of the hex — so
     re-theming is editing values here, never sweeping 1200 lines.
     ========================================================================== */

  /* --- ground: page, mid stop, raised --- */
  --bg-0:#f8f4ee;
  --bg-1:#f0e9e0;
  --bg-2:#ffffff;

  /* --- glass: the card primitive.
     Light and dark run the pane in opposite directions — in light it is a
     white sheet *over* a warm ground and needs a high alpha to read as a
     pane at all; in dark it is a faint white wash over near-black. Same
     token, inverted mechanics, which is exactly why it is a token. --- */
  --glass:rgba(255,255,255,0.55);
  --glass-strong:rgba(255,255,255,0.76);
  --border:rgba(255,255,255,0.85);
  --border-strong:rgba(44,40,34,0.16);
  /* the 1px lit edge along the top of every glass pane */
  --glass-sheen:rgba(255,255,255,0.9);

  /* --- neutral fills that are NOT the glass pane: inset fields, ghost
     buttons, progress tracks, muted badges. Kept apart from --glass so a
     field inside a card can sit *below* the card in light and *above* it in
     dark without either theme fighting the other. --- */
  --fill-1:rgba(255,255,255,0.5);
  --fill-2:rgba(255,255,255,0.7);
  --track:rgba(44,40,34,0.10);
  --fill-mute:rgba(44,40,34,0.09);

  /* --- accents. The *-rgb triplets exist so a tinted fill can be written
     rgba(var(--accent-rgb),0.12) instead of re-hardcoding the hue. --- */
  --fayrouzeh:#46806f;         /* مریم‌گلی sage — primary accent */
  --fayrouzeh-dim:#35655a;
  --accent-rgb:70,128,111;
  --mohr:#b26a55;              /* clay — status / signature accent only */
  --mohr-dim:#8f5241;
  --mohr-rgb:178,106,85;
  --gold:#b8863f;              /* burnt sand — streak / highlight, sparingly */
  --gold-rgb:184,134,63;

  /* --- foreground on a *filled* accent surface. A pastel fill needs dark
     ink and a deep fill needs light ink, so this flips with the theme and
     every filled control reads it instead of guessing. --- */
  --on-accent:#ffffff;
  --on-gold:#2b1d06;
  --on-finished:#ffffff;
  --danger-text:#a8503c;

  /* --- challenge status: شروع نشده / در حال اجرا / تمام شده ---
     One hue per derived status (see challenge_status in
     app/routers/challenge.py), shared by the card's accent stripe, its pill,
     and the matching button in the status filter, so the same colour always
     means the same thing. Sage is already "live/primary" everywhere in this
     UI, so «در حال اجرا» keeps it; «شروع نشده» takes gold (waiting, not
     wrong -- red would read as a problem); «تمام شده» goes grey and steps
     back, because a finished challenge is context, not an invitation. */
  /* --- one hue per challenge category; see the dark block for the rule.
     Same six hues, run at the lightness a *light* ground needs (OKLCH ~.58)
     so the icon reads as ink rather than as a highlighter, and --on-cat
     flips to white because the filled tile is now the dark surface. --- */
  --cat-fitness:#518c3b;      --cat-fitness-rgb:81,140,59;
  --cat-nutrition:#b16512;    --cat-nutrition-rgb:177,101,18;
  --cat-mental:#8965ba;       --cat-mental-rgb:137,101,186;
  --cat-productivity:#407dbf; --cat-productivity-rgb:64,125,191;
  --cat-social:#b05788;       --cat-social-rgb:176,87,136;
  --cat-other:#6b7c8f;        --cat-other-rgb:107,124,143;
  --on-cat:#ffffff;

  /* --- admin scope: the chrome that says "you are in the panel". A third
     colour channel on purpose, and deliberately *chrome only* -- it paints
     the topbar rule, the ambient wash and the mode strip, and never a card,
     a pill or a swatch. Status (--st-*) and category (--cat-*) keep their
     surfaces; this one only ever paints the frame around them, so nothing
     on a page has to be read as two channels at once. --- */
  --admin:#4f5d9f;            --admin-rgb:79,93,159;

  /* The status channel. Its one surface is «چراغ وضعیت» (.st-lamp) and the
     status filter buttons -- nothing paints a card edge, border or ground
     with these any more. The rgb triples are what the lamp reads: it tints
     its own ring, fill and open ground from one number, so a status needs
     one hue here rather than three near-identical rules downstream. */
  --st-active:#46806f;        --st-active-rgb:70,128,111;
  --st-upcoming:#b8863f;      --st-upcoming-rgb:184,134,63;
  --st-finished:#7a7167;      --st-finished-rgb:122,113,103;
  /* The ground the lamp frosts itself against over a cover photograph. */
  --lamp-ground-rgb:248,244,238;

  --text-1:#2c2822;
  --text-2:#6b6255;
  --text-3:#7a7164;

  /* --- translucent chrome: topbar, bottom nav, sheets, scrim. Tinted with
     the page ground rather than plain black/white, so content blurring
     underneath keeps the theme's temperature. --- */
  --chrome-bg:rgba(248,244,238,0.7);
  --chrome-bg-strong:rgba(248,244,238,0.88);
  --sheet-bg:rgba(252,250,246,0.94);
  --scrim:rgba(44,40,34,0.32);
  /* the tour's frosted pane over everything *but* the step it is explaining.
     It is not --scrim: a scrim only has to darken, while this one is blurred
     and so has to keep the ground's temperature -- on the sand theme a black
     wash under a blur turns the page grey and the spotlight looks cut out of
     a different app. Alpha is low because the blur is doing the hiding. */
  --tour-veil:rgba(240,233,224,0.26);

  /* --- ambient wash (.app-bg): two radial glows plus a tile texture. A data
     URI cannot read a var, so the whole url() is the token and each theme
     ships its own stroke colour. --- */
  --amb-accent:0.14;
  --amb-mohr:0.10;
  --tile-opacity:.05;
  --tile-url:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='84' viewBox='0 0 84 84'%3E%3Cg fill='none' stroke='%2346806f' stroke-width='1'%3E%3Cpath d='M42 0 L84 42 L42 84 L0 42 Z'/%3E%3Ccircle cx='42' cy='42' r='18'/%3E%3C/g%3E%3C/svg%3E");

  /* --- glow: the halo an accent-filled control casts on the ground. Light
     gets a tighter, weaker halo (a glow on a bright ground reads as smudge
     long before it reads as light); dark can afford the bloom. -lift is the
     hover/raised variant, not a second colour. --- */
  --glow-accent:0 0 18px rgba(var(--accent-rgb),0.28);
  --glow-accent-lift:0 8px 26px rgba(var(--accent-rgb),0.34);
  --glow-mohr:0 0 16px rgba(var(--mohr-rgb),0.24);
  --glow-gold:0 0 16px rgba(var(--gold-rgb),0.26);
  /* text-shadow twin, for a number or word rendered *in* the accent colour */
  --glow-text:0 0 14px rgba(var(--accent-rgb),0.22);

  /* --- corner radius: one ladder, four steps plus the pill. Every rounded
     corner in the file reads one of these; a raw px radius is a fifth step
     nobody named. --radius-pill is a shape, not a size — it is 999px so a
     capsule stays a capsule at any height. --- */
  --radius-pill:999px;
  --radius-lg:22px;
  --radius-md:16px;
  --radius-sm:10px;
  --radius-xs:4px;

  /* --- type scale. Six steps carry every piece of *text* in the app
     (--fs-xs … --fs-2xl); the last two are display sizes — a glyph, an
     initial, one big number — and are deliberately outside the reading
     ladder rather than pretending to be part of it. In rem, so the scale
     follows the reader's own browser setting. --- */
  --fs-xs:.65rem;      /* ~10.4px */
  --fs-sm:.72rem;      /* ~11.5px */
  --fs-md:.78rem;      /* ~12.5px */
  --fs-lg:.875rem;     /* 14px    */
  --fs-xl:1rem;        /* 16px    */
  --fs-2xl:1.1875rem;  /* 19px    */
  --fs-3xl:1.375rem;   /* 22px    */
  --fs-4xl:1.75rem;    /* 28px    */

  /* --- spacing ladder, 2px..24px. --space-N is N*2 px, so the name says the
     step and the step says the pixels; anything reaching for an odd value
     was an eyeball nudge and rounds to the nearest step. Geometry that is
     not spacing (an icon's 18px box, a 56px FAB) keeps its own number. --- */
  --space-1:2px;   --space-2:4px;   --space-3:6px;   --space-4:8px;
  --space-5:10px;  --space-6:12px;  --space-7:14px;  --space-8:16px;
  --space-9:18px;  --space-10:20px; --space-11:22px; --space-12:24px;

  /* --- stacking order. The whole app's layering, in one place and in the
     order it paints: page chrome, then the FAB over it, then the immersive
     reader, then the transient layers (toast, sheet, tour) over everything.
     A raw z-index anywhere else is a layer nobody can see from here. The two
     values that ride one step above a layer (the rail's close button, the
     image picker over an open sheet) say so with calc rather than a number
     that would drift if the layer moved. --- */
  --z-sticky:20;           /* detail tabs, floating back button */
  --z-topbar:25;           /* sits above them on purpose */
  --z-nav:30;              /* bottom nav, sticky CTA */
  --z-fab:40;
  --z-immersive:50;        /* the rail reader, over the page it came from */
  --z-toast:60;
  --z-sheet-backdrop:70;
  --z-sheet:71;
  --z-tour:80;             /* the tour veil and the explainer popover */

  /* --- motion: two durations. Fast is a control answering a finger, base is
     a surface moving. Anything longer than these is an animation with its own
     character (a breathing ring, a spinner) and keeps its own timing. --- */
  --dur-fast:.15s;
  --dur-base:.2s;

  --shadow-soft:0 8px 24px rgba(76,66,52,0.10);
  --shadow-lift:0 16px 36px rgba(76,66,52,0.16);

  --ease:cubic-bezier(.2,.7,.2,1);

  /* --- press: the tap animation every control in the app shares. Geometry
     and easing are theme-independent and live here; --press-lit (what "the
     finger is on it" looks like) is a surface colour and so is restated in
     the two dark blocks. See the "Press feedback" section below. --- */
  --press-scale:.965;                          /* how far a control sinks */
  --ease-press:cubic-bezier(.3,.75,.4,1);      /* down: quick, no bounce */
  --ease-press-out:cubic-bezier(.3,1.1,.4,1);  /* up: soft, overshoots once */
  /* Both lit states are the same filter list so the two can interpolate --
     a mismatched list makes the browser swap them abruptly instead. On the
     sand ground a white card cannot get much brighter, so light leans on
     saturation; on the night ground brightness does the work. */
  --press-lit-off:brightness(1) saturate(1);
  --press-lit:brightness(1.05) saturate(1.16);
  --hover-lit:brightness(1.02) saturate(1.07);

  /* --- bottom nav geometry: single source of truth ---
     10px top padding + 21px icon + 4px gap + ~13px label + 10px base bottom
     padding, plus the device inset. Everything that has to clear the nav
     (shell padding, FAB, toast, sticky CTA) reads this instead of repeating
     its own magic number. */
  --nav-h:calc(58px + env(safe-area-inset-bottom));
  /* breathing room between the last element of a page and whatever closes it
     (the bottom nav, or the viewport edge on nav-less pages). Owned by .shell
     so no page has to remember it; anything that overrides .shell's
     padding-bottom must add it back. */
  --page-end-gap:14px;
  --cta-h:72px; /* .sticky-cta's own rendered height (padding + button) */
  /* width of the mobile shell; anything that spans or aligns to it (nav,
     sticky CTA, toast, sheet, FAB) reads this instead of repeating the
     literal width. */
  --shell-max:480px;

  color-scheme:light;
}

/* --------------------------------------------------------------------------
   Dark theme «شب کرمی» (creamy night) — the *same room* as the sand theme with
   the lamp switched off, not a second palette: the ground keeps the light
   theme's warm hue and only drops in lightness (#f8f4ee -> #16120d,
   #f0e9e0 -> #1e1913, #ffffff -> #272018), and every wash over it is tinted
   with warm cream rgba(240,224,198,a) rather than the blue-white a neutral
   overlay would give. The accents are the light theme's own hues pulled up
   into pastel so they read on a dark ground without neon. Written twice on
   purpose — once for a device that asks
   for dark (guarded so an explicit light choice still wins), once for the
   [data-theme] the toggle writes. Only the selector differs; keep the two
   value lists in sync when adding a token.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --bg-0:#16120d;
    --bg-1:#1e1913;
    --bg-2:#272018;

    --glass:rgba(240,224,198,0.05);
    --glass-strong:rgba(240,224,198,0.09);
    --border:rgba(242,228,205,0.13);
    --border-strong:rgba(242,228,205,0.22);
    --glass-sheen:rgba(250,238,220,0.12);

    --fill-1:rgba(240,224,198,0.045);
    --fill-2:rgba(240,224,198,0.07);
    --track:rgba(240,224,198,0.09);
    --fill-mute:rgba(240,224,198,0.13);

    --fayrouzeh:#7fc9ad;
    --fayrouzeh-dim:#5da88f;
    --accent-rgb:127,201,173;
    --mohr:#eda893;
    --mohr-dim:#cd8878;
    --mohr-rgb:237,168,147;
    --gold:#e3c07c;
    --gold-rgb:227,192,124;

    --on-accent:#0e2a22;
    --on-gold:#35270a;
    --on-finished:#16120d;
    --danger-text:#ef9d92;


    /* --- one hue per challenge category (app/models/challenge.py). All six
       sit at the same OKLCH lightness (~.78) and chroma (~.10) so no category
       shouts louder than another, and the hues are spaced far enough apart to
       stay tellable at thumb size. The accent sage is *not* in the set: it
       means "primary/live" everywhere else and a category may not borrow it.
       --on-cat is the ink on a *filled* category surface (the detail hero
       tile), the --on-accent of this scale. --- */
    --cat-fitness:#9ac98a;      --cat-fitness-rgb:154,201,138;
    --cat-nutrition:#e8b183;    --cat-nutrition-rgb:232,177,131;
    --cat-mental:#c7a9e0;       --cat-mental-rgb:199,169,224;
    --cat-productivity:#8fbde6; --cat-productivity-rgb:143,189,230;
    --cat-social:#e5a1bf;       --cat-social-rgb:229,161,191;
    --cat-other:#b3a898;        --cat-other-rgb:179,168,152;
    --on-cat:#16120d;

    /* admin scope chrome -- see :root */
    --admin:#a9b4ee;            --admin-rgb:169,180,238;

    --st-active:#7fc9ad;        --st-active-rgb:127,201,173;
    --st-upcoming:#e3c07c;      --st-upcoming-rgb:227,192,124;
    --st-finished:#a29684;      --st-finished-rgb:162,150,132;
    --lamp-ground-rgb:22,18,13;

    --text-1:#f2ebe0;
    --text-2:#b6a894;
    --text-3:#948673;

    --chrome-bg:rgba(22,18,13,0.70);
    --chrome-bg-strong:rgba(22,18,13,0.88);
    --sheet-bg:rgba(39,32,24,0.95);
    --scrim:rgba(10,7,4,0.68);
    --tour-veil:rgba(30,25,19,0.31);

    --amb-accent:0.20;
    --amb-mohr:0.13;
    --tile-opacity:.05;
    --tile-url:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='84' viewBox='0 0 84 84'%3E%3Cg fill='none' stroke='%237fc9ad' stroke-width='1'%3E%3Cpath d='M42 0 L84 42 L42 84 L0 42 Z'/%3E%3Ccircle cx='42' cy='42' r='18'/%3E%3C/g%3E%3C/svg%3E");

    --glow-accent:0 0 26px rgba(var(--accent-rgb),0.26);
    --glow-accent-lift:0 10px 30px rgba(var(--accent-rgb),0.28);
    --glow-mohr:0 0 22px rgba(var(--mohr-rgb),0.22);
    --glow-gold:0 0 22px rgba(var(--gold-rgb),0.22);
    --glow-text:0 0 18px rgba(var(--accent-rgb),0.26);

    --press-lit:brightness(1.24) saturate(1.06);
    --hover-lit:brightness(1.10) saturate(1.03);

    --shadow-soft:0 10px 30px rgba(0,0,0,0.45);
    --shadow-lift:0 16px 40px rgba(0,0,0,0.55);

    color-scheme:dark;
  }
}

:root[data-theme="dark"]{
  --bg-0:#16120d;
  --bg-1:#1e1913;
  --bg-2:#272018;

  --glass:rgba(240,224,198,0.05);
  --glass-strong:rgba(240,224,198,0.09);
  --border:rgba(242,228,205,0.13);
  --border-strong:rgba(242,228,205,0.22);
  --glass-sheen:rgba(250,238,220,0.12);

  --fill-1:rgba(240,224,198,0.045);
  --fill-2:rgba(240,224,198,0.07);
  --track:rgba(240,224,198,0.09);
  --fill-mute:rgba(240,224,198,0.13);

  --fayrouzeh:#7fc9ad;
  --fayrouzeh-dim:#5da88f;
  --accent-rgb:127,201,173;
  --mohr:#eda893;
  --mohr-dim:#cd8878;
  --mohr-rgb:237,168,147;
  --gold:#e3c07c;
  --gold-rgb:227,192,124;

  --on-accent:#0e2a22;
  --on-gold:#35270a;
  --on-finished:#16120d;
  --danger-text:#ef9d92;

  /* --- one hue per challenge category (app/models/challenge.py). All six
     sit at the same OKLCH lightness (~.78) and chroma (~.10) so no category
     shouts louder than another, and the hues are spaced far enough apart to
     stay tellable at thumb size. The accent sage is *not* in the set: it
     means "primary/live" everywhere else and a category may not borrow it.
     --on-cat is the ink on a *filled* category surface (the detail hero
     tile), the --on-accent of this scale. --- */
  --cat-fitness:#9ac98a;      --cat-fitness-rgb:154,201,138;
  --cat-nutrition:#e8b183;    --cat-nutrition-rgb:232,177,131;
  --cat-mental:#c7a9e0;       --cat-mental-rgb:199,169,224;
  --cat-productivity:#8fbde6; --cat-productivity-rgb:143,189,230;
  --cat-social:#e5a1bf;       --cat-social-rgb:229,161,191;
  --cat-other:#b3a898;        --cat-other-rgb:179,168,152;
  --on-cat:#16120d;

  /* admin scope chrome -- see :root */
  --admin:#a9b4ee;            --admin-rgb:169,180,238;

  --st-active:#7fc9ad;        --st-active-rgb:127,201,173;
  --st-upcoming:#e3c07c;      --st-upcoming-rgb:227,192,124;
  --st-finished:#a29684;      --st-finished-rgb:162,150,132;
  --lamp-ground-rgb:22,18,13;

  --text-1:#f2ebe0;
  --text-2:#b6a894;
  --text-3:#948673;

  --chrome-bg:rgba(22,18,13,0.70);
  --chrome-bg-strong:rgba(22,18,13,0.88);
  --sheet-bg:rgba(39,32,24,0.95);
  --scrim:rgba(10,7,4,0.68);
  --tour-veil:rgba(30,25,19,0.31);

  --amb-accent:0.20;
  --amb-mohr:0.13;
  --tile-opacity:.05;
  --tile-url:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='84' viewBox='0 0 84 84'%3E%3Cg fill='none' stroke='%237fc9ad' stroke-width='1'%3E%3Cpath d='M42 0 L84 42 L42 84 L0 42 Z'/%3E%3Ccircle cx='42' cy='42' r='18'/%3E%3C/g%3E%3C/svg%3E");

  --glow-accent:0 0 26px rgba(var(--accent-rgb),0.26);
  --glow-accent-lift:0 10px 30px rgba(var(--accent-rgb),0.28);
  --glow-mohr:0 0 22px rgba(var(--mohr-rgb),0.22);
  --glow-gold:0 0 22px rgba(var(--gold-rgb),0.22);
  --glow-text:0 0 18px rgba(var(--accent-rgb),0.26);

  --press-lit:brightness(1.24) saturate(1.06);
  --hover-lit:brightness(1.10) saturate(1.03);

  --shadow-soft:0 10px 30px rgba(0,0,0,0.45);
  --shadow-lift:0 16px 40px rgba(0,0,0,0.55);

  color-scheme:dark;
}

*{box-sizing:border-box;}
html,body{margin:0;padding:0;}

body{
  font-family:'Vazirmatn', sans-serif;
  background:var(--bg-0);
  color:var(--text-1);
  direction:rtl;
  min-height:100vh;
  -webkit-font-smoothing:antialiased;
}

/* ambient background: two coloured glows + geometric tile texture. The glow
   alphas and the tile's stroke are theme tokens -- on the light ground the
   wash has to be *stronger* than on the dark one to be visible at all, which
   is why --amb-* is a token rather than a fixed 0.16. */
.app-bg{
  position:fixed; inset:0; z-index:-1; overflow:hidden;
  background:
    radial-gradient(60% 40% at 80% -10%, rgba(var(--accent-rgb),var(--amb-accent)), transparent 60%),
    radial-gradient(50% 35% at 10% 110%, rgba(var(--mohr-rgb),var(--amb-mohr)), transparent 60%),
    linear-gradient(180deg, var(--bg-0), var(--bg-1) 60%, var(--bg-0));
}
.app-bg::before{
  content:"";
  position:absolute; inset:0;
  opacity:var(--tile-opacity);
  background-image:var(--tile-url);
  background-size:84px 84px;
}


a{color:inherit; text-decoration:none;}
button{font-family:inherit;}
ul{list-style:none; margin:0; padding:0;}

/* ==========================================================================
   Press feedback — the one tap animation every control in the app shares
   --------------------------------------------------------------------------
   A tap compresses the control and lets it spring back out, and while the
   finger is down the surface reads a shade brighter and more saturated. It is
   deliberately one system rather than an `:active` rule per component, which
   is what this file had: the scales had drifted apart (.93 on an icon button,
   .985 on a card) and most controls — chips, tabs, list rows, nav items,
   sheet options — answered a tap with nothing at all.

   Two mechanics keep it from colliding with what is already here:

   - It animates the standalone `scale` property, never `transform`. Several
     controls own their transform for layout or hover (.fab is anchored with
     one, .bottom-nav and .toast centre themselves with one, .ap-item lifts on
     hover), and `scale` composes with those instead of overwriting them.
   - It runs as a keyframe animation, not a transition. A component that sets
     `transition:transform …` on a *class* always outweighs a global rule on a
     bare element selector, so a shared transition would silently fail on
     exactly the controls that already have one — and adding `scale` to each
     component's transition list would put this timing back in twelve places.

   The pressed class comes from initPressFeedback() in app.js, not from
   `:active`: `:active` never fires on iOS without a touch listener, it drops
   the moment a tap turns into a scroll, and a flick tap flashes it for a
   single frame. The JS holds the squeeze for a minimum beat and cancels it
   when the gesture turns out to be a scroll.
   ========================================================================== */

@keyframes press-in{
  from{scale:1;                  filter:var(--press-lit-off);}
  to  {scale:var(--press-scale); filter:var(--press-lit);}
}
/* the spring: back past 1 by a third of the distance it sank, then settle */
@keyframes press-out{
  0%  {scale:var(--press-scale); filter:var(--press-lit);}
  60% {scale:calc(1 + (1 - var(--press-scale)) / 3);}
  100%{scale:1;                  filter:var(--press-lit-off);}
}

.is-pressed{animation:press-in .11s var(--ease-press) forwards;}
.is-releasing{animation:press-out .34s var(--ease-press-out);}

/* Per-component depth. One scale cannot serve a 40px round button and a
   full-width card: the same *percentage* reads as a firm click on the first
   and as the card falling over on the second, so the sink is tuned by size
   and the timing stays shared. */
.icon-btn, .btn-nav, .fab, .cta-side, .tl-action, button.avatar-profile,
.back-btn{--press-scale:.92;}
.chip, .tab, .dtab, .btn-seg, .auth-tab, .category-pill, .jp-cell, .ap-item,
.btn-ghost, .nav-item{--press-scale:.94;}
.challenge-card, .menu-item, .setting-row, .dash-link, .tl-row{--press-scale:.985;}
.btn-primary{--press-scale:.975;}

/* A pointer that can hover gets the same "lit" cue on approach, at half
   strength. Zero-specificity :where() so every component's own :hover rule
   still wins — this only adds the filter none of them set. */
@media (hover:hover) and (pointer:fine){
  :where(button, a[href], summary, [role="button"], [role="tab"],
         [role="radio"], [role="option"], label[for], [data-press]):hover{
    filter:var(--hover-lit);
  }
}

/* Reduced motion keeps the feedback and drops the movement: the lit state is
   information (this is the thing you are touching), the squeeze is decoration. */
@media (prefers-reduced-motion:reduce){
  .is-pressed{animation:none; filter:var(--press-lit);}
  .is-releasing{animation:none;}
}

/* --- app shell: mobile-first PWA frame, centers on wider screens --- */
.shell{
  max-width:var(--shell-max);
  margin:0 auto;
  min-height:100vh;
  position:relative;
  padding-bottom:calc(var(--page-end-gap) + env(safe-area-inset-bottom));
}
/* only pages that actually render a bottom nav need clearance for it -- the
   nav already carries the device inset, so this adds the gap on top of it */
.shell:has(.bottom-nav){padding-bottom:calc(var(--nav-h) + var(--page-end-gap));}

/* --- glass card primitive ---
   The inset hairline is what makes the pane read as glass rather than as a
   flat translucent rectangle: it is the light catching the top edge, so it
   sits *inside* the border and is a theme token (a bright sheen on a light
   ground would be invisible, on a dark one it is the whole effect). */
.glass{
  background:var(--glass);
  border:1px solid var(--border);
  border-radius:var(--radius-lg);
  backdrop-filter:blur(20px) saturate(140%);
  -webkit-backdrop-filter:blur(20px) saturate(140%);
  box-shadow:var(--shadow-soft), inset 0 1px 0 var(--glass-sheen);
}

/* --- top bar ---
   [back] [where you are] [actions], and nothing else. It sticks to the top so
   the back control and the page actions stay reachable down a long list
   instead of scrolling away, and it is translucent for the same reason the
   nav is: content passing under it should still read as content. The brand
   mark it used to open with is gone -- an app the visitor is already inside
   does not need to introduce itself once per page. The logo still exists on
   the one screen where it earns its place: auth.html. */
.topbar{
  position:sticky; top:0; z-index:var(--z-topbar);
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-6);
  padding:var(--space-7) var(--space-10) var(--space-6);
  background:var(--chrome-bg);
  backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
  border-bottom:1px solid var(--border);
}
/* min-width:0 is what lets the title inside actually shrink and ellipsize --
   without it a flex item refuses to go below its min-content width and a long
   challenge name pushes the actions off the edge. */
.topbar-lead{display:flex; align-items:center; gap:var(--space-5); flex:1; min-width:0;}
/* The topbar is space-between, so multiple trailing actions have to be one
   flex child or they get pushed to opposite ends of the header. */
.topbar-actions{display:flex; align-items:center; gap:var(--space-3); flex-shrink:0;}
.brand-mark{
  width:38px; height:38px; border-radius:50%;
  display:grid; place-items:center;
  background:radial-gradient(circle at 35% 30%, var(--fayrouzeh), var(--fayrouzeh-dim));
  box-shadow:var(--glow-accent-lift);
}
/* the mark's glyph is stroke="currentColor" in auth.html so it can flip with
   the theme -- a presentation attribute cannot read a custom property */
.brand-mark svg{width:20px; height:20px; color:var(--on-accent);}

/* Every icon in the app is an inline <svg> injected into a [data-icon] span
   by renderIcons(). An inline svg sits on a text baseline, so its span is a
   line box tall enough for a descender that is never drawn -- which is what
   pushed the glyph a couple of pixels above centre in every round control
   (.fab, .icon-btn, .back-btn). Making the wrapper a flex box removes the
   line box entirely; inline-flex so the icons that sit beside text in a row
   keep flowing as they did. */
[data-icon]{display:inline-flex; align-items:center; justify-content:center;}
[data-icon] > svg{display:block;}

.icon-btn{
  position:relative;
  width:40px; height:40px; border-radius:50%;
  display:grid; place-items:center;
  background:var(--glass); border:1px solid var(--border);
  color:var(--text-2); cursor:pointer;
  transition:transform var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.icon-btn:hover{background:var(--glass-strong); color:var(--text-1);}
.icon-btn svg{width:18px; height:18px;}
.icon-btn.has-dot::after{
  content:""; position:absolute; width:8px; height:8px; border-radius:50%;
  background:var(--mohr); border:2px solid var(--bg-0);
  transform:translate(11px,-11px);
}
/* The notification bell's unread count. A number rather than the bare dot
   above, because "something happened" and "eleven things happened" are
   different messages and the bell is the only place either is delivered.
   It is `hidden` until app.js has a count, so a member with nothing unread
   -- and a page that could not reach the server -- gets a plain bell rather
   than a badge reading zero.

   Placed to match `.has-dot` exactly, with a physical corner rather than a
   logical one: the two marks sit on the same control in the same header, and
   one flipping sides per direction while the other did not would read as a
   bug. The ring is `--bg-0`, so the badge lifts off whatever passes under
   the translucent topbar. */
.icon-badge{
  position:absolute; top:-4px; right:-4px;
  min-width:18px; height:18px; padding:0 var(--space-3); border-radius:var(--radius-pill);
  display:grid; place-items:center;
  font-size:var(--fs-xs); font-weight:800; line-height:1;
  background:var(--mohr); color:var(--on-accent);
  border:2px solid var(--bg-0);
  /* The count is decoration on top of the link's own label, which app.js
     rewrites to carry the number -- announcing it twice is worse than once. */
  pointer-events:none;
}
.icon-badge[hidden]{display:none;}

/* --- greeting / hero --- */
.hero{padding:var(--space-3) var(--space-10) var(--space-9);}
.hero-eyebrow{font-size:var(--fs-lg); color:var(--text-3); margin:0 0 var(--space-2);}
.hero-title{font-size:var(--fs-3xl); font-weight:800; margin:0; line-height:1.5;}
.hero-title b{color:var(--fayrouzeh); font-weight:800; text-shadow:var(--glow-text);}

/* --- stats row --- */
.stats{
  display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5);
  padding:0 var(--space-10) var(--space-10);
}
.stat-card{
  padding:var(--space-7) var(--space-5); text-align:center; border-radius:var(--radius-md);
}
.stat-num{font-size:var(--fs-2xl); font-weight:800; display:block;}
.stat-label{font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-2); display:block;}
.stat-card.accent .stat-num{color:var(--fayrouzeh); text-shadow:var(--glow-text);}
.stat-card.mohr .stat-num{color:var(--mohr); text-shadow:var(--glow-mohr);}
.stat-card.gold .stat-num{color:var(--gold); text-shadow:var(--glow-gold);}

/* --- section header --- */
.section-head{
  display:flex; align-items:center; justify-content:space-between;
  padding:var(--space-2) var(--space-10) var(--space-6);
}
.section-head h2{font-size:var(--fs-xl); font-weight:700; margin:0;}
.tabs{display:flex; gap:var(--space-3); background:var(--glass); border:1px solid var(--border); border-radius:var(--radius-pill); padding:var(--space-2);}
.tab{
  padding:var(--space-3) var(--space-7); border-radius:var(--radius-pill); font-size:var(--fs-md); color:var(--text-2);
  cursor:pointer; border:none; background:transparent; transition:all var(--dur-base) var(--ease);
}
.tab.active{background:var(--fayrouzeh); color:var(--on-accent); font-weight:700; box-shadow:var(--glow-accent);}

/* --- challenge card --- */
.challenge-list{display:flex; flex-direction:column; gap:var(--space-6); padding:0 var(--space-10);}

.challenge-card{
  position:relative; overflow:hidden;
  padding:var(--space-8) var(--space-8) var(--space-7);
  border-radius:var(--radius-lg);
  transition:transform var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}

.cc-top{display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-5);}
/* ==========================================================================
   Primitive: .pill -- the small labelled capsule
   --------------------------------------------------------------------------
   One shape, one set of metrics, and a modifier per *meaning*: a tag, a
   role, a queue answer, an invite's derived state. The base is the neutral
   one, because that is what a label says when it carries no channel;
   .pill-tag is the accent one. Status is deliberately *not* among them any
   more: it left the pill for «چراغ وضعیت» (.st-lamp), so --st-* reaches no
   capsule at all and --cat-* is the one colour channel a pill carries.
   ========================================================================== */
.pill{
  display:inline-flex; align-items:center; gap:var(--space-2);
  font-size:var(--fs-sm); font-weight:700;
  padding:var(--space-2) var(--space-5); border-radius:var(--radius-pill);
  background:var(--fill-1); color:var(--text-3); border:1px solid var(--border);
}
.pill svg{width:12px; height:12px; flex-shrink:0;}
/* the accent label: the commonest pill in the app */
.pill-tag{
  color:var(--fayrouzeh);
  background:rgba(var(--accent-rgb),0.12); border-color:rgba(var(--accent-rgb),0.28);
}
.cc-title{font-size:var(--fs-xl); font-weight:700; margin:var(--space-5) 0 var(--space-2); line-height:1.6;}
/* Whole-card hit area. The title stays the only real link (so it keeps its
   href, focus ring and middle-click), but its ::after is stretched over the
   whole card -- tapping anywhere that isn't an action opens the challenge.
   The card is already position:relative, which is what the inset:0 resolves
   against; .cc-actions is lifted above the overlay so its buttons still win
   the click. */
.cc-title a{color:inherit; text-decoration:none;}
.cc-title a::after{content:""; position:absolute; inset:0; z-index:1;}
.cc-desc{font-size:var(--fs-md); color:var(--text-3); margin:0 0 var(--space-6); line-height:1.7;}

.cc-meta{display:flex; align-items:center; gap:var(--space-7); font-size:var(--fs-sm); color:var(--text-2);}
.cc-meta-item{display:flex; align-items:center; gap:var(--space-3);}
.cc-meta-item svg{width:13px; height:13px; opacity:.8;}

/* Today's check-in window: a fixed label from the server ("تا پایان امروز")
   plus a live remainder that renderDeadlines() in app.js writes every minute.
   The remainder ships empty, so :empty hides it -- separator and all -- when
   JS hasn't run yet or the occurrence has no closing instant at all. */
.cc-window{white-space:nowrap;}
.cc-remaining{white-space:nowrap; font-variant-numeric:tabular-nums;}
.cc-remaining::before{content:"·"; margin-inline-end:var(--space-3); opacity:.55;}
.cc-remaining:empty{display:none;}
.cc-remaining.is-urgent{color:var(--gold); font-weight:600;}
.cc-remaining.is-passed{color:var(--mohr); font-weight:600;}

.cc-progress{
  height:6px; border-radius:var(--radius-pill); background:var(--track);
  overflow:hidden; margin-top:var(--space-6);
}
.cc-progress > i{
  display:block; height:100%; border-radius:var(--radius-pill);
  background:linear-gradient(90deg, var(--fayrouzeh-dim), var(--fayrouzeh));
  box-shadow:var(--glow-accent);
}

.cc-actions{display:flex; gap:var(--space-4); margin-top:var(--space-7); position:relative; z-index:2;}

/* ==========================================================================
   Primitive: .btn -- everything that is tapped and carries a label
   --------------------------------------------------------------------------
   The base is deliberately thin: the UA's own button chrome undone, a row of
   icon-then-label centred, and the app's font. It is *not* the glass button
   -- half the buttons in this app carry no chrome at all (the heart, the
   conversation, the emoji key, a segment of a .seg), and a base that painted
   a border and a ground would make each of them start by undoing it.
   The chrome is .btn-ghost, and everything else is a modifier beside it.
   ========================================================================== */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--space-3);
  padding:0; border:0; background:none; color:inherit;
  font-family:inherit; font-size:var(--fs-md); font-weight:700;
  text-align:center; text-decoration:none; cursor:pointer;
}
.btn svg{width:14px; height:14px;}

/* the glass button: the app's default "do this" control */
.btn-ghost{
  display:flex; flex:1; padding:var(--space-5) var(--space-5); border-radius:var(--radius-sm);
  border:1px solid var(--border); background:var(--glass); color:var(--text-1);
  transition:background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn-ghost:hover{background:var(--glass-strong);}
.btn-danger{color:var(--danger-text); border-color:rgba(var(--mohr-rgb),0.38);}
.btn-danger:hover{background:rgba(var(--mohr-rgb),0.14);}

/* --- مهر seal — signature element for completed challenges --- */
.seal{
  position:absolute; left:14px; top:14px;
  width:56px; height:56px; border-radius:50%;
  display:grid; place-items:center;
  transform:rotate(-14deg);
  border:2px solid var(--mohr);
  box-shadow:0 0 0 3px rgba(var(--mohr-rgb),0.12), inset 0 0 0 3px rgba(var(--mohr-rgb),0.08), var(--glow-mohr);
  opacity:.92;
  pointer-events:none;
}
.seal::before{
  content:""; position:absolute; inset:5px; border-radius:50%;
  border:1px dashed rgba(var(--mohr-rgb),0.55);
}
.seal span{
  font-size:var(--fs-xs); font-weight:800; color:var(--mohr); letter-spacing:.5px;
  text-align:center; line-height:1.25;
}
.challenge-card.completed{border-color:rgba(var(--mohr-rgb),0.25);}
.challenge-card.completed .cc-title{padding-inline-end:60px;}

/* «لیست» wears the same glass the rail card does: the opaque pane is dropped
   so the page ground reads through, and only the hairline, the sheen and a
   whisper of blur are left to say there is a surface here at all. */
.challenge-list:not(.rail) .challenge-card.glass{
  background:transparent;
  backdrop-filter:blur(1.4px); -webkit-backdrop-filter:blur(1.4px);
  /* with no pane behind it the edge is the whole object, so it is lit twice:
     the sheen along the top and a hairline of the same light all round,
     bleeding a little outside -- glass catching light, not a neon tube */
  border-color:color-mix(in srgb, var(--glass-sheen) 45%, var(--border));
  box-shadow:inset 0 1px 0 var(--glass-sheen),
             inset 0 0 12px -6px var(--glass-sheen),
             0 0 10px -4px var(--glass-sheen);
}

/* --- empty state --- */
.empty{
  text-align:center; padding:48px var(--space-12); color:var(--text-3);
}
.empty svg{width:44px; height:44px; margin-bottom:var(--space-6); opacity:.5;}
.empty p{margin:0; font-size:var(--fs-lg); line-height:1.8;}

/* --- floating action button --- */
.fab{
  position:fixed; bottom:calc(var(--nav-h) + var(--page-end-gap));
  /* anchored 20px inside the shell's left edge, clamped to the viewport so it
     stays reachable on phones narrower than the shell */
  left:calc(50% - min(var(--shell-max), 100vw) / 2 + 20px);
  transform:none;
  width:56px; height:56px; border-radius:50%;
  display:grid; place-items:center; cursor:pointer;
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  box-shadow:var(--glow-accent-lift), var(--shadow-lift);
  border:none; color:var(--on-accent);
  transition:transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
  z-index:var(--z-fab);
}
.fab:hover{transform:translateY(-2px);}
.fab svg{width:24px; height:24px;}

/* --- bottom nav --- */
.bottom-nav{
  position:fixed; bottom:0; left:50%; transform:translateX(-50%);
  width:100%; max-width:var(--shell-max);
  display:flex; justify-content:space-around; align-items:center;
  padding:var(--space-5) var(--space-6) calc(var(--space-5) + env(safe-area-inset-bottom));
  background:var(--chrome-bg);
  border-top:1px solid var(--border);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  z-index:var(--z-nav);
}
/* Five destinations now, so an item takes an equal share of the bar and its
   own padding is the part that gives: at 360px the old per-item padding put
   the five labels past the shell's width. `min-width:0` is what lets the
   share actually shrink, and the label stays on one line. */
.nav-item{
  display:flex; flex-direction:column; align-items:center; gap:var(--space-2);
  flex:1 1 0; min-width:0; white-space:nowrap;
  color:var(--text-3); font-size:var(--fs-xs); cursor:pointer; padding:var(--space-2) var(--space-3);
  transition:color var(--dur-fast) var(--ease);
}
.nav-item svg{width:21px; height:21px;}
.nav-item.active{color:var(--fayrouzeh);}
/* the icon is an SVG stroke, so its halo is a drop-shadow filter rather
   than the text-shadow the accent *words* use */
.nav-item.active svg{filter:drop-shadow(0 0 7px rgba(var(--accent-rgb),0.55));}

/* Wide screens are handled in one place, at the foot of this file:
   see «ستون گوشی روی صفحه‌های عریض». There is deliberately no desktop
   layout here -- above the breakpoint the app stays the same phone, in a
   column, rather than growing a second set of geometry to keep in step. */

/* ==========================================================================
   Shared: back button (inner pages)
   ========================================================================== */
.back-btn{
  width:38px; height:38px; border-radius:50%;
  display:grid; place-items:center; cursor:pointer;
  background:var(--glass); border:1px solid var(--border); color:var(--text-1);
}
.back-btn svg{width:18px; height:18px;}
.back-btn[hidden]{display:none;}
/* error.html / auth.html don't extend layout.html, so they have no topbar to
   hang a back control in. inset-inline-start is the right edge under RTL,
   matching where {% block topbar_leading %} puts it on every other page. */
.floating-back{position:fixed; inset-block-start:18px; inset-inline-start:18px; z-index:var(--z-sticky);}

/* --- breadcrumb trail (lives inside the topbar) ---
   Rendered by initNavTrail() in app.js from the pages actually visited, so it
   can be any length up to TRAIL_MAX. It stacks into two lines rather than
   running as one: the steps *behind* the visitor are a small dim path on top,
   and the last crumb -- this page -- is the header's title underneath, at
   title weight. One element therefore answers both "what is this page" and
   "what is behind it", which is why the brand name it replaced is not missed.
   A root page has no ancestors and renders the title line alone. Long titles
   ellipsize and the path line scrolls rather than wrapping, because a third
   line would push the content down on every page. */
.crumbs{display:flex; flex-direction:column; gap:1px; min-width:0;}
/* display:flex would beat the UA's [hidden]{display:none}. */
.crumbs[hidden]{display:none;}
.crumb-path{
  display:flex; align-items:center; gap:var(--space-3);
  overflow-x:auto; white-space:nowrap; scrollbar-width:none;
  font-size:var(--fs-sm); color:var(--text-3); line-height:1.5;
}
.crumb-path::-webkit-scrollbar{display:none;}
.crumb{flex-shrink:0; color:var(--text-3); text-decoration:none; transition:color var(--dur-fast) var(--ease);}
a.crumb:hover{color:var(--fayrouzeh);}
a.crumb:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px; border-radius:var(--radius-xs);}
/* The current crumb and the create wizard's static heading are the same
   object -- the page's own name, in the header -- so they share one rule and
   cannot drift apart. */
.crumb.current,
.topbar-title{
  margin:0;
  font-size:var(--fs-xl); font-weight:800; letter-spacing:.2px; line-height:1.5;
  color:var(--text-1);
  /* A challenge title can be 50 characters; it truncates rather than wrapping
     the header onto a second line or shoving the actions off the edge. */
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* ==========================================================================
   Admin scope — "am I in the panel, or just browsing?"
   --------------------------------------------------------------------------
   The admin screens reuse the member-facing primitives on purpose
   (.settings-page, .setting-row, the challenge list's .seg and .search-bar),
   so nothing *inside* the page tells an operator which mode they are in. The
   answer goes in the topbar, which is sticky and therefore on screen at every
   scroll position of every admin page: the action slot's glyph, and the
   colour of the page's own name in the trail.

   Both are still, not pulsing. A marker that moves forever is read once and
   then filtered out by the eye, and it would have to be switched off under
   prefers-reduced-motion -- which is to say it would be missing for the
   readers least able to spare it. Persistence does the work instead.

   --admin is chrome only. It never paints a card, a pill or a swatch, so a
   roster row's status (--st-*) and category (--cat-*) colours keep meaning
   exactly what they mean on every other screen.
   ========================================================================== */

/* The shield that replaces the bell. Tinted rather than merely coloured: at
   18px a glyph swap alone is a difference you have to look for, and the whole
   job of this control is being noticed without being looked for. */
.icon-btn.admin-mark{
  background:rgba(var(--admin-rgb),0.12);
  border-color:rgba(var(--admin-rgb),0.38);
  color:var(--admin);
}
.icon-btn.admin-mark:hover{
  background:rgba(var(--admin-rgb),0.20);
  color:var(--admin);
}

/* ...and the page's own name beside it. Only the *current* crumb takes the
   hue -- the trail behind it may well run back through member-facing pages,
   and colouring those would say they were part of the panel. The glow is the
   --glow-text pattern in the admin hue: enough to catch the eye on arrival,
   not enough to compete with the shield. */
body[data-admin-scope] .crumb.current{
  color:var(--admin);
  text-shadow:0 0 14px rgba(var(--admin-rgb),0.28);
}

.crumb-sep{flex-shrink:0; display:grid; place-items:center; color:var(--text-3); opacity:.55;}
.crumb-sep svg{width:11px; height:11px;}

/* Category/cadence icon riding inside a filter chip. */
.chip{display:inline-flex; align-items:center; gap:var(--space-3);}
.chip svg{width:13px; height:13px; flex-shrink:0;}

/* ==========================================================================
   Auth (login / register)
   ========================================================================== */
.auth-wrap{
  min-height:100vh; display:flex; flex-direction:column;
  justify-content:center; padding:32px var(--space-12);
}
.auth-logo{text-align:center; margin-bottom:28px;}
.auth-logo .brand-mark{margin:0 auto var(--space-7); width:56px; height:56px;}
.auth-logo .brand-mark svg{width:28px; height:28px;}
.auth-logo h1{font-size:var(--fs-2xl); font-weight:800; margin:0 0 var(--space-3);}
.auth-logo p{font-size:var(--fs-lg); color:var(--text-3); margin:0;}

.auth-card{padding:var(--space-11) var(--space-10);}

.auth-tabs{
  display:flex; gap:var(--space-3); background:var(--fill-2);
  border:1px solid var(--border); border-radius:var(--radius-pill); padding:var(--space-2); margin-bottom:var(--space-11);
}
.auth-tab{
  flex:1; text-align:center; padding:var(--space-5) 0; border-radius:var(--radius-pill);
  font-size:var(--fs-lg); font-weight:700; color:var(--text-2); cursor:pointer;
  border:none; background:transparent; transition:all var(--dur-base) var(--ease);
}
.auth-tab.active{background:var(--fayrouzeh); color:var(--on-accent);}

.field{margin-bottom:var(--space-7);}
.field label{
  display:block; font-size:var(--fs-md); color:var(--text-2); margin-bottom:var(--space-4); font-weight:600;
}
.field-input{
  display:flex; align-items:center; gap:var(--space-4);
  background:var(--fill-1); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:0 var(--space-6);
  transition:border-color var(--dur-fast) var(--ease);
}
.field-input:focus-within{border-color:var(--fayrouzeh);}
.field-input svg{width:17px; height:17px; color:var(--text-3); flex-shrink:0;}
.field-input input, .field-input select, .field-input textarea{
  flex:1; border:none; background:transparent; outline:none;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); padding:var(--space-6) var(--space-1);
  min-width:0;
}
.field-input textarea{resize:vertical; line-height:1.6;}
/* A textarea is multi-line, so the flex centring that suits a one-line input
   would float the label-adjacent affordances against its middle. */
.field-input:has(textarea){align-items:stretch;}
.field-input input::placeholder{color:var(--text-3);}
.field-toggle{cursor:pointer; color:var(--text-3);}
/* One line under a sheet or wizard field saying what the choice means --
   same type scale as a settings row's .sr-hint, since it does the same job.
   A choice whose consequence is not on screen is a choice made blind. */
.field-note{margin:var(--space-3) var(--space-1) 0; font-size:var(--fs-sm); color:var(--text-3); line-height:1.7;}
/* The operator roster's «ناشناس» mark. Not a colour of its own: it is
   neither a status nor a category, and those two channels never share a
   surface -- weight and ink are enough to make it scannable. */
.anon-mark{color:var(--text-2); font-weight:600;}

/* --- avatar picker (signup) ---
   A 40-tile grid would push the submit button off-screen, so the grid scrolls
   inside a fixed-height well and the button stays reachable. The tiles are
   flat SVGs with their own palettes, so the *selection* has to be carried by
   the frame around them -- an accent ring plus a lift -- and never by a tint
   over the picture. */
.field-hint{font-weight:500; color:var(--text-3);}
.avatar-pick{
  display:grid; grid-template-columns:repeat(5, 1fr); gap:var(--space-5);
  max-height:232px; overflow-y:auto; padding:var(--space-6);
  background:var(--fill-1); border:1px solid var(--border); border-radius:var(--radius-sm);
  overscroll-behavior:contain;
}
.ap-item{
  appearance:none; border:none; padding:0; cursor:pointer; background:transparent;
  display:block; border-radius:50%; overflow:hidden; position:relative;
  box-shadow:0 0 0 2px var(--border);
  transition:box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
/* The square comes off the image, not the button: sizing the button by
   aspect-ratio while its only child is height:100% leaves the row track with
   nothing to measure, and the tiles overlap. */
.ap-item img{width:100%; height:auto; aspect-ratio:1; display:block; object-fit:cover;}
.ap-item:hover{transform:translateY(-1px); box-shadow:0 0 0 2px var(--fayrouzeh-dim);}
.ap-item:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:3px;}
.ap-item.selected{
  box-shadow:0 0 0 3px var(--fayrouzeh), var(--glow-accent-lift);
  transform:translateY(-1px);
}
@media (max-width:360px){.avatar-pick{grid-template-columns:repeat(4, 1fr);}}
/* the same grid inside a sheet, where the panel is already scrolling and a
   232px window would strand the tiles in a scroller inside a scroller */
.sheet-avatars{max-height:min(46vh, 320px); background:transparent; border:none; padding:var(--space-1);}

.btn-primary{
  display:flex; width:100%; padding:var(--space-7); border-radius:var(--radius-sm);
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  color:var(--on-accent); font-size:var(--fs-lg); font-weight:800;
  box-shadow:var(--glow-accent-lift); gap:var(--space-4);
  margin-top:var(--space-3); transition:transform var(--dur-fast) var(--ease);
}
.btn-primary svg{width:16px; height:16px;}

.auth-foot{text-align:center; margin-top:var(--space-9); font-size:var(--fs-md); color:var(--text-3);}
.auth-foot a{color:var(--fayrouzeh); font-weight:700;}

/* --- SMS one-time code ---
   Two steps on one card rather than two screens: the second step has to keep
   naming the number the code went to, and the commonest failure in this flow
   is a typo in that number -- a member who has navigated away from it cannot
   check. `.otp-target` is therefore a line, not a heading, with the correction
   right next to it.

   Everything here is neutral. Colour in this app carries status and category
   and nothing else (CLAUDE.md, "Colour carries two channels"), and a login
   screen is neither; the accent is spent only on the one control the member is
   meant to press, exactly as `.btn-primary` already does. */
.otp-target{
  display:flex; align-items:center; justify-content:center; gap:var(--space-4);
  flex-wrap:wrap; margin:0 0 var(--space-9); font-size:var(--fs-md); color:var(--text-3);
}
.otp-target b{color:var(--text-1); font-weight:700; direction:ltr; unicode-bidi:isolate;}
.otp-target button{
  border:none; background:transparent; padding:var(--space-1) var(--space-2); cursor:pointer;
  font-family:inherit; font-size:var(--fs-md); font-weight:700; color:var(--fayrouzeh);
}

/* The boxes run left-to-right inside an RTL page because a number does:
   the first digit of the code the member reads in the SMS has to be the box
   their cursor starts in, or every paste and every autofill lands reversed. */
.otp-code{
  display:grid; grid-template-columns:repeat(6, 1fr); gap:var(--space-4);
  direction:ltr; margin-bottom:var(--space-8);
}
.otp-digit{
  width:100%; aspect-ratio:1/1.15; text-align:center;
  background:var(--fill-1); border:1px solid var(--border);
  border-radius:var(--radius-sm); color:var(--text-1);
  font-family:inherit; font-size:var(--fs-2xl); font-weight:800;
  outline:none; padding:0; min-width:0;
  transition:border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.otp-digit:focus{border-color:var(--fayrouzeh); background:var(--fill-2);}
.otp-digit.filled{border-color:var(--fayrouzeh-dim);}
/* Chrome/Safari draw spinners on a numeric field, which would put two tiny
   arrows inside a 44px box that is only ever typed into. */
.otp-digit::-webkit-outer-spin-button, .otp-digit::-webkit-inner-spin-button{
  -webkit-appearance:none; margin:0;
}
.otp-digit{-moz-appearance:textfield; appearance:textfield;}

/* The countdown is the *whole* control until it reaches zero: offering a
   resend button that answers 429 would be a control that lies. */
.otp-resend{
  text-align:center; margin-top:var(--space-7); font-size:var(--fs-md); color:var(--text-3);
}
.otp-resend button{
  border:none; background:transparent; cursor:pointer; padding:var(--space-2);
  font-family:inherit; font-size:var(--fs-md); font-weight:700; color:var(--fayrouzeh);
}
.otp-resend button[disabled]{color:var(--text-3); cursor:default; font-weight:600;}

/* Errors are inline and persistent, not a toast: a toast that fades takes the
   only explanation of a refusal with it, and this is the one screen a member
   cannot navigate past to go and re-read it. `--mohr` is the app's existing
   refusal colour. */
.auth-error{
  display:none; margin:0 0 var(--space-7); padding:var(--space-5) var(--space-6);
  border-radius:var(--radius-sm); font-size:var(--fs-md); line-height:1.7;
  background:rgba(var(--mohr-rgb), .12); color:var(--mohr);
  border:1px solid rgba(var(--mohr-rgb), .28);
}
.auth-error.show{display:block;}

/* The other way in. It is a link under the card rather than a third tab,
   because signing in by SMS is the path essentially everybody takes and a
   pair of equally-weighted tabs would ask a question most members have no
   reason to answer. */
.auth-alt{text-align:center; margin-top:var(--space-8);}
.auth-alt button{
  border:none; background:transparent; cursor:pointer; padding:var(--space-3);
  font-family:inherit; font-size:var(--fs-md); font-weight:700; color:var(--text-2);
  display:inline-flex; align-items:center; gap:var(--space-3);
}
.auth-alt button svg{width:15px; height:15px;}

/* ==========================================================================
   Explore (global challenge list)
   ========================================================================== */
.search-bar{
  display:flex; align-items:center; gap:var(--space-4); margin:0 var(--space-10) var(--space-7);
  background:var(--fill-1); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:0 var(--space-6);
}
.search-bar svg{width:17px; height:17px; color:var(--text-3);}
.search-bar input{
  flex:1; border:none; background:transparent; outline:none;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); padding:var(--space-6) var(--space-1);
}
.btn-filter{
  width:38px; height:38px; border-radius:var(--radius-sm); flex-shrink:0;
  background:var(--glass); border:1px solid var(--border);
  color:var(--text-2);
}
.btn-filter svg{width:16px; height:16px;}

/* Filter chips wrap onto as many rows as they need: the category set is small
   and fixed, so every option stays visible instead of scrolling off-screen. */
.chip-row{
  display:flex; flex-wrap:wrap; gap:var(--space-4); padding:0 var(--space-10) var(--space-8);
}
.chip{
  flex:0 0 auto; padding:var(--space-4) var(--space-8); border-radius:var(--radius-pill); font-size:var(--fs-md); font-weight:600;
  background:var(--glass); border:1px solid var(--border); color:var(--text-2); cursor:pointer;
  white-space:nowrap; transition:all var(--dur-base) var(--ease);
}
.chip.active{background:var(--fayrouzeh); border-color:var(--fayrouzeh); color:var(--on-accent); font-weight:700; box-shadow:var(--glow-accent);}
.chip:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px;}

/* --- "my challenges" segmented filter (challenge list) ---
   Three mutually exclusive states, so a segmented control rather than more
   chips: chips read as an additive multi-select next to the category row. */
.seg-row{padding:0 var(--space-10) var(--space-7);}
/* A row that carries an explainer becomes a row: the dot sits beside the
   segmented control it explains, not under it. */
.seg-row:has(> .explain-dot){display:flex; align-items:center;}
.seg{
  display:flex; gap:var(--space-2); background:var(--glass); border:1px solid var(--border);
  border-radius:var(--radius-pill); padding:var(--space-2);
}
.btn-seg{
  flex:1 1 0; min-width:0; display:flex;
  padding:var(--space-4) var(--space-4); border-radius:var(--radius-pill);
  font-weight:600; color:var(--text-2);
  white-space:nowrap; transition:all var(--dur-base) var(--ease);
}
.btn-seg svg{flex-shrink:0;}
.btn-seg:hover{color:var(--text-1);}
.btn-seg.active{background:var(--fayrouzeh); color:var(--on-accent); font-weight:700; box-shadow:var(--glow-accent);}
.btn-seg:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px;}

/* --- infinite scroll: sentinel + loading indicator --- */
.scroll-sentinel{height:1px;}
/* display:flex below would otherwise beat the UA's [hidden]{display:none},
   leaving "در حال بارگذاری" on screen forever once the last page has loaded
   (or when a filter matched nothing and no page was ever fetched). */
.list-loading[hidden]{display:none;}
.list-loading{
  display:flex; align-items:center; justify-content:center; gap:var(--space-4);
  padding:var(--space-9) 0; color:var(--text-3); font-size:var(--fs-md);
}
.list-loading .spinner{
  width:16px; height:16px; border-radius:50%;
  border:2px solid var(--border); border-top-color:var(--fayrouzeh);
  animation:spin .7s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg);}}

/* --- toast --- */
.toast{
  position:fixed; left:50%; bottom:calc(var(--nav-h) + var(--page-end-gap)); transform:translate(-50%, 12px);
  max-width:calc(var(--shell-max) - 40px); padding:var(--space-5) var(--space-9); border-radius:var(--radius-pill);
  background:var(--sheet-bg); border:1px solid var(--border-strong);
  color:var(--text-1); font-size:var(--fs-md); font-weight:600; text-align:center;
  box-shadow:var(--shadow-lift); opacity:0; pointer-events:none; z-index:var(--z-toast);
  transition:opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.toast.visible{opacity:1; transform:translate(-50%, 0);}

.explore-card{
  display:flex; align-items:center; gap:var(--space-6);
}
.explore-thumb{
  width:52px; height:52px; border-radius:var(--radius-md); flex-shrink:0;
  display:grid; place-items:center;
  background:linear-gradient(145deg, rgba(var(--accent-rgb),0.18), rgba(var(--accent-rgb),0.06));
  color:var(--fayrouzeh);
}
.explore-thumb svg{width:24px; height:24px;}

/* Cadence type at a glance: a small corner badge on the category thumb, so the
   kind of challenge (one-off / scheduled / recurring) reads without opening it.
   Same icon set as the `cadence_icon` filter feeds everywhere else. */
.explore-thumb{position:relative;}
/* Primitive: .badge -- a small bordered mark that is a shape rather than a
   label. Two members: the cadence glyph riding on a thumb's corner, and the
   timeline's four-state square. A pill is the wrong base for either, because
   neither of them carries words. */
.badge{
  display:grid; place-items:center; flex:none;
  border:1px solid var(--border-strong); background:var(--bg-2); color:var(--text-1);
}
.badge-cadence{
  position:absolute; inset-block-end:-5px; inset-inline-start:-5px;
  width:20px; height:20px; border-radius:50%;
}
.badge-cadence svg{width:11px; height:11px;}

/* --- the challenge's own picture ---------------------------------------
   One element, three sizes: the 52px list thumb, the rail's 4:3 cover and
   the reader's full screen. The picture is painted *inside* `.explore-thumb`
   rather than replacing it, so everything already hung off that element --
   the cadence badge, the rail's scrim, the status pill's placement -- keeps
   working, and a challenge with no picture keeps exactly the
   category-coloured placeholder it has always had.

   `object-fit:cover` is the crop the member already saw and approved in the
   picker (static/js/imagepick.js), which is the whole reason that step
   exists: an uncropped photo is cut here silently. */
.explore-thumb > .et-cover{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; border-radius:inherit; display:block;
}
/* the category glyph is the placeholder, so it goes when there is something
   to place; the tile texture goes with it, being noise over a photograph */
.explore-thumb.has-cover > [data-icon]:not(.badge-cadence){display:none;}
.challenge-list.rail .explore-thumb.has-cover::before{display:none;}
/* The tall one has no layout box outside the reader, so `loading="lazy"`
   never fetches it while a list is being scrolled -- which is the whole
   reason both can ship in one markup. */
.et-tall{display:none;}
.explore-thumb.has-tall:not(.has-square) .et-tall{display:block;}
.challenge-list.is-immersive .has-tall .et-tall{display:block;}
.challenge-list.is-immersive .has-tall .et-sq{display:none;}
/* over a photograph the scrim has to carry the body's ink on its own, so it
   runs deeper and starts higher than it does over a flat gradient */
.challenge-list.rail .explore-thumb.has-cover::after{
  background:linear-gradient(to top, var(--bg-2) 8%, rgba(0,0,0,0.28) 55%, transparent 85%);
}
.challenge-list.is-immersive .explore-thumb.has-cover::after{
  background:linear-gradient(to top, var(--bg-2) 20%, rgba(0,0,0,0.34) 62%, transparent 88%);
}
.detail-icon .badge-cadence{
  inset-block-end:-4px; inset-inline-start:-4px;
  width:22px; height:22px;
  background:var(--bg-1); color:var(--fayrouzeh);
}
.detail-icon .badge-cadence svg{width:12px; height:12px; color:inherit;}
.explore-body{flex:1; min-width:0;}
.explore-body h3{font-size:var(--fs-lg); font-weight:700; margin:0 0 var(--space-2);}
.explore-body p{font-size:var(--fs-sm); color:var(--text-3); margin:0; display:flex; gap:var(--space-6); align-items:center;}
.explore-body p svg{width:12px; height:12px;}
/* ==========================================================================
   Challenge detail
   ========================================================================== */
/* The hero's pane fades out from the top centre: solid where the icon and
   the title sit, dissolving to a ~8% ghost at the side and bottom edges, so
   the card reads as a wash of glass the page comes *out* of rather than as a
   rectangle stuck onto it. The falloff is the same emanating shape as the
   category glow above it, one radius wider, so the two agree.

   It is a mask on a pane *layer*, not on the element: masking .detail-hero
   itself would fade the title and the numbers with it. So the element hands
   its .glass paint over to ::after -- background, hairline, sheen and the
   backdrop blur together, since a blur that outlived the pane would stay as a
   visible smudge past the edge where there is no longer any glass to blur.
   The outer drop shadow goes: a pane whose own edge is a quarter opaque
   cannot cast a hard shadow onto the page. Where mask-image is unsupported the layer
   simply paints whole, which is the flat card this replaced.
   ::after is behind the content (z-index:-1, contained by the isolate) and
   behind ::before, so the category wash still reads on top of it. */
.detail-hero{
  margin:0 var(--space-10) var(--space-9); padding:26px var(--space-10); text-align:center; position:relative; overflow:hidden;
  isolation:isolate;
  --hero-fade:radial-gradient(68% 96% at 50% 0%,
    #000 0%, #000 30%, rgba(0,0,0,0.78) 56%, rgba(0,0,0,0.45) 80%, rgba(0,0,0,0.25) 100%);
  /* The rim: one hairline whose *brightness* varies down the card instead of
     one uniform bright outline. It opens on --glass-sheen (the "lit edge of
     glass" token) and has settled onto the ordinary --border hairline within
     the first fifth of the height -- so the light reads as a reflection
     arriving from above and catching the top edge, then sliding off down the
     sides, rather than as a drawn frame. The line itself is 1px everywhere;
     nothing about it is thick. */
  --hero-rim:linear-gradient(to bottom,
    var(--glass-sheen) 0%, var(--glass-sheen) 3%, var(--border) 20%, var(--border) 100%);
}
/* the .glass paint is moved to ::after below; the border stays as transparent
   ink so the element keeps the exact box it had */
.detail-hero.glass{
  background:none; border-color:transparent; box-shadow:none;
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
.detail-hero.glass::after{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  border-radius:inherit;
  /* the classic two-background ring: the pane paints to the padding box, the
     rim gradient to the border box, so a 1px transparent border *is* the line.
     A gradient cannot be drawn with box-shadow, and the alternative -- a stack
     of identical rings compounding one faint alpha -- is what made the edge
     read as a hard frame instead of a reflection. */
  border:1px solid transparent;
  background:
    linear-gradient(var(--glass), var(--glass)) padding-box,
    var(--hero-rim) border-box;
  backdrop-filter:blur(20px) saturate(140%);
  -webkit-backdrop-filter:blur(20px) saturate(140%);
  -webkit-mask-image:var(--hero-fade);
  mask-image:var(--hero-fade);
}
.detail-hero::before{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:radial-gradient(60% 80% at 50% -10%, rgba(var(--accent-rgb),0.18), transparent 70%);
  -webkit-mask-image:var(--hero-fade);
  mask-image:var(--hero-fade);
}
/* THE HERO'S PICTURE. It sits behind the glass pane (`::after`, z-index:-1)
   and wears the same --hero-fade mask, so it dissolves at the edges exactly
   like everything else in this card instead of arriving as a rectangle stuck
   onto the page. Its own scrim is inside the wrapper -- an <img> has no
   pseudo-element -- and is what keeps --text-* ink legible over whatever was
   photographed, in both themes, without a second palette. */
.hero-cover{
  position:absolute; inset:0; z-index:-2; border-radius:inherit;
  overflow:hidden; pointer-events:none;
  -webkit-mask-image:var(--hero-fade);
  mask-image:var(--hero-fade);
}
.hero-cover img{width:100%; height:100%; object-fit:cover; display:block;}
/* The pane over it thins out: a 20px backdrop blur is right over the page's
   own ground and turns a photograph into a smear. The rim, the mask and the
   fade are untouched -- only how much of the picture the glass keeps. Opacity
   is denser at the centre (85%) and, via --hero-fade, thins toward the edges
   (down to ~50%) so the photo is never fully hidden and gets glassier at the
   rim. */
.detail-hero.has-cover.glass::after{
  background:
    linear-gradient(color-mix(in srgb, var(--glass) 85%, transparent),
                    color-mix(in srgb, var(--glass) 85%, transparent)) padding-box,
    var(--hero-rim) border-box;
  backdrop-filter:blur(3px) saturate(120%);
  -webkit-backdrop-filter:blur(3px) saturate(120%);
}

.detail-icon{
  width:56px; height:56px; border-radius:var(--radius-md); margin:0 auto var(--space-7);
  display:grid; place-items:center; position:relative; z-index:1;
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  box-shadow:var(--glow-accent-lift);
}
.detail-icon svg{width:26px; height:26px; color:var(--on-accent);}
.detail-hero h1{position:relative; z-index:1; font-size:var(--fs-2xl); font-weight:800; margin:0 0 var(--space-4);}
.detail-hero .pill{position:relative; z-index:1;}

.detail-stats{
  display:grid; grid-template-columns:repeat(3,1fr);
  margin:var(--space-8) 0 0; position:relative; z-index:1;
}
/* The stats sit in the pane's faded tail, so their dividers fade with it --
   a hairline at full strength there would draw the edge the card is busy
   dissolving. Same colour token, laid down as a gradient instead of a border. */
.detail-stats div{text-align:center; position:relative;}
.detail-stats div + div::before{
  content:""; position:absolute; inset-block:0; inset-inline-start:0; width:1px;
  background:linear-gradient(to bottom, var(--border), transparent);
}
.detail-stats b{display:block; font-size:var(--fs-xl); font-weight:800;}
.detail-stats span{font-size:var(--fs-sm); color:var(--text-3);}
.detail-stats .gold b{color:var(--gold); display:flex; align-items:center; justify-content:center; gap:var(--space-2);}
.detail-stats .gold b svg{width:14px; height:14px;}

.detail-section{padding:0 var(--space-10) var(--space-9);}
.detail-section h2{font-size:var(--fs-lg); font-weight:700; margin:0 0 var(--space-5);}
.detail-section p{font-size:var(--fs-lg); color:var(--text-2); line-height:1.9; margin:0;}

.rule-list{display:flex; flex-direction:column; gap:var(--space-5);}
.rule-item{
  display:flex; align-items:flex-start; gap:var(--space-5); padding:var(--space-6) var(--space-7);
}
.rule-item svg{width:16px; height:16px; color:var(--fayrouzeh); flex-shrink:0; margin-top:1px;}
.rule-item span{font-size:var(--fs-md); color:var(--text-2); line-height:1.7;}

/* ==========================================================================
   Primitive: .avatar -- one person's picture, at five sizes
   --------------------------------------------------------------------------
   An opaque ground is part of the base, not a note repeated at every surface:
   several catalogue styles ship a transparent SVG (bottts-neutral, cameo,
   glyphs) and `_default.svg` is a bare silhouette in a ring, so whatever sits
   behind the <img> would otherwise become part of the picture. A translucent
   --fill-* token would let it back through, which is why this is --bg-1.
   The element is also the ground for initials where there is no picture,
   hence place-items:center rather than a bare overflow:hidden.
   ========================================================================== */
.avatar{
  border-radius:50%; overflow:hidden; flex-shrink:0;
  background:var(--bg-1); display:grid; place-items:center;
  font-weight:700;
  /* the roster rows are a bare <img class="avatar">, with no wrapper to crop
     them, so the fit is on the base rather than only on the child */
  object-fit:cover;
}
.avatar > img{width:100%; height:100%; display:block; object-fit:cover;}
.avatar-xs{width:28px; height:28px;}
.avatar-sm{width:30px; height:30px; font-size:var(--fs-xs);}
.avatar-md{width:34px; height:34px;}
.avatar-lg{width:36px; height:36px;}
.avatar-xl{width:84px; height:84px;}
/* the roster rows want a rounded square, not a circle */
.avatar-square{border-radius:var(--radius-sm);}

.avatar-stack{display:flex; align-items:center;}
.avatar-stack .avatar{border:2px solid var(--bg-0); margin-inline-start:-10px;}
.avatar-stack .avatar:first-child{margin-inline-start:0;}


/* --- history: occurrence timeline ---
   One row per occurrence the cadence actually asks for, newest first --
   NOT one per calendar day. A weekly challenge has ~1 occurrence a week,
   so a day grid would render 6/7 empty cells per row. */
.tl-row{
  display:flex; align-items:center; gap:var(--space-6); padding:var(--space-6) var(--space-7);
  border-bottom:1px solid var(--border); cursor:pointer; min-height:52px;
}
.tl-row:last-child{border-bottom:none;}
.tl-row.today{background:rgba(var(--accent-rgb),0.06);}
.tl-row:active{background:var(--fill-1);}

.badge-mark{
  width:28px; height:28px; border-radius:var(--radius-sm);
  font-size:var(--fs-lg); font-weight:700; line-height:1;
  border-color:var(--border); background:var(--fill-2); color:var(--text-3);
}
.tl-row.completed .badge-mark{background:var(--fayrouzeh); border-color:var(--fayrouzeh); color:var(--bg-0);}
.tl-row.completed .badge-mark::before{content:"✓";}
.tl-row.skipped .badge-mark{background:var(--fill-mute); border-color:var(--border-strong); color:var(--text-2);}
.tl-row.skipped .badge-mark::before{content:"–";}
.tl-row.missed .badge-mark{background:rgba(var(--mohr-rgb),0.20); border-color:rgba(var(--mohr-rgb),0.5); color:var(--danger-text);}
.tl-row.missed .badge-mark::before{content:"✕";}
.tl-row.pending .badge-mark{background:rgba(var(--gold-rgb),0.14); border-color:var(--gold); color:var(--gold);}
.tl-row.pending .badge-mark::before{content:"•";}

.tl-main{flex:1; min-width:0;}
.tl-date{font-size:var(--fs-lg); font-weight:600; color:var(--text-1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.tl-meta{font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}
.tl-row.completed .tl-meta{color:var(--fayrouzeh);}
.tl-row.missed .tl-meta{color:var(--danger-text);}

.tl-action{
  flex:none; border:1px solid rgba(var(--accent-rgb),0.4); background:rgba(var(--accent-rgb),0.12);
  color:var(--fayrouzeh); border-radius:var(--radius-pill); font-family:inherit; font-size:var(--fs-md); font-weight:700;
  padding:var(--space-4) var(--space-8); cursor:pointer;
}
.tl-action.solid{width:100%; border-radius:var(--radius-sm); padding:var(--space-5); margin-top:var(--space-5);}

/* --- history: recurring_quota period rows ---
   A quota has no per-day slot of its own, so the period is the unit. */
.quota-history{display:flex; flex-direction:column; gap:var(--space-4);}
.quota-row{
  background:var(--glass); border:1px solid var(--border); border-radius:var(--radius-md);
  padding:var(--space-6) var(--space-7);
}
.quota-row.current{background:rgba(var(--accent-rgb),0.06); border-color:rgba(var(--accent-rgb),0.25);}
.quota-row-head{display:flex; align-items:center; justify-content:space-between; font-size:var(--fs-md); margin-bottom:var(--space-5);}
.quota-row-label{font-weight:600;}
.quota-row.current .quota-row-label{color:var(--fayrouzeh);}
.quota-row-count{color:var(--text-3); font-size:var(--fs-sm);}
.quota-pips{display:flex; gap:var(--space-2);}
.quota-pip{flex:1; height:8px; border-radius:var(--radius-xs); background:var(--track);}
.quota-pip.done{background:var(--fayrouzeh);}

.sticky-cta{
  /* floats above the bottom nav rather than welding onto it: the same
     --page-end-gap that keeps page content off the nav also separates this
     panel from the nav and from the shell edges, so it reads as a card and
     not as a second footer. The device safe-area inset is already carried by
     .bottom-nav below it, so it is not repeated here. Its rendered height is
     --cta-h; anything clearing it must add the gap above *and* below. */
  position:fixed; bottom:calc(var(--nav-h) + var(--page-end-gap));
  left:50%; transform:translateX(-50%);
  width:calc(100% - 2 * var(--page-end-gap));
  max-width:calc(var(--shell-max) - 2 * var(--page-end-gap));
  padding:var(--space-7) var(--space-10);
  background:var(--chrome-bg-strong);
  border:1px solid var(--border); border-radius:var(--radius-lg);
  box-shadow:var(--shadow-soft);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px); z-index:var(--z-nav);
}

/* --- bottom sheet (check-in confirm/skip) --- */
.sheet-backdrop{
  position:fixed; inset:0; background:var(--scrim);
  z-index:var(--z-sheet-backdrop); animation:sheetFadeIn var(--dur-base) var(--ease);
}
.sheet{
  position:fixed; inset:0; z-index:var(--z-sheet);
  display:flex; align-items:flex-end; justify-content:center;
  pointer-events:none;
}
.sheet-panel{
  pointer-events:auto;
  width:100%; max-width:var(--shell-max);
  padding:var(--space-9) var(--space-10) calc(var(--space-9) + env(safe-area-inset-bottom));
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  background:var(--bg-1); border:1px solid var(--border-strong); border-bottom:none;
  box-shadow:var(--shadow-lift);
  max-height:88vh; overflow-y:auto;
  animation:sheetSlideUp .22s var(--ease);
}
.sheet-head{
  display:flex; align-items:center; justify-content:space-between;
  margin-bottom:var(--space-7);
}
.sheet-head h3{font-size:var(--fs-xl); font-weight:800; margin:0;}
.sheet-body{display:flex; flex-direction:column; gap:var(--space-7); margin:0;}
/* A sheet's own read-only paragraph: what the reader has to have in front of
   them before they press the button. Body-scale rather than `.field-note`'s
   hint scale, because this *is* the sheet's content and not an aside on a
   control; `is-danger` tints it with the ink a destructive label already
   wears, so the colour says nothing new. */
.sheet-note{margin:0; font-size:var(--fs-md); line-height:1.9; color:var(--text-2);}
.sheet-note.is-danger{color:var(--danger-text);}
.sheet-actions{display:flex; gap:var(--space-5); margin-top:var(--space-9);}
.sheet-actions button{flex:1;}
.sheet-actions .primary{
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  border-color:transparent; color:var(--on-accent);
  box-shadow:var(--glow-accent);
}
/* Separated from .sheet-actions by a rule so a destructive action is never
   sitting flush against "save" -- these sheets are one mis-tap wide. */
.sheet-danger{
  margin-top:var(--space-7); padding-top:var(--space-7); border-top:1px solid var(--border);
  display:flex;
}
@keyframes sheetFadeIn{from{opacity:0;} to{opacity:1;}}
@keyframes sheetSlideUp{from{transform:translateY(100%);} to{transform:translateY(0);}}

/* ==========================================================================
   Create challenge
   ========================================================================== */
.textarea-field{
  background:var(--fill-1); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:var(--space-6); transition:border-color var(--dur-fast) var(--ease);
}
.textarea-field:focus-within{border-color:var(--fayrouzeh);}
.textarea-field textarea{
  width:100%; border:none; background:transparent; outline:none; resize:none;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); line-height:1.8; min-height:80px;
}
.textarea-field textarea::placeholder{color:var(--text-3);}

.category-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-4);}
.category-pill{
  padding:var(--space-6) var(--space-3); text-align:center; border-radius:var(--radius-sm);
  background:var(--glass); border:1px solid var(--border); cursor:pointer;
  font-size:var(--fs-sm); color:var(--text-2); transition:all var(--dur-base) var(--ease);
}
.category-pill svg{width:18px; height:18px; display:block; margin:0 auto var(--space-3);}
.category-pill.active{background:rgba(var(--accent-rgb),0.12); border-color:var(--fayrouzeh); color:var(--fayrouzeh); box-shadow:var(--glow-accent);}

.stepper{display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-11); padding:0 var(--space-10);}
.step{display:flex; flex-direction:column; align-items:center; gap:var(--space-3); flex:1; position:relative;}
.step-dot{
  width:26px; height:26px; border-radius:50%; display:grid; place-items:center;
  background:var(--glass); border:1px solid var(--border); font-size:var(--fs-sm); font-weight:700; color:var(--text-3);
  z-index:1;
}
.step.active .step-dot{background:var(--fayrouzeh); border-color:var(--fayrouzeh); color:var(--on-accent);}
.step.done .step-dot{background:var(--fayrouzeh-dim); border-color:var(--fayrouzeh-dim); color:var(--on-accent);}
.step span{font-size:var(--fs-xs); color:var(--text-3);}
.step.active span{color:var(--text-1);}
.step:not(:last-child)::after{
  content:""; position:absolute; top:13px; right:calc(50% + 18px); left:calc(-50% + 18px);
  height:2px; background:var(--border); z-index:0;
}
.step.done:not(:last-child)::after{background:var(--fayrouzeh-dim);}

/* ==========================================================================
   Profile
   ========================================================================== */
.profile-head{text-align:center; padding:var(--space-5) var(--space-10) var(--space-12);}
.avatar-profile{
  margin:0 auto var(--space-6); position:relative; overflow:visible;
  font-size:var(--fs-4xl); font-weight:800; color:var(--text-1);
  box-shadow:var(--glow-accent-lift);
}
/* On your own profile the picture is a <button> that opens the avatar sheet;
   on anyone else's it stays a plain <div>, so the reset lives here rather
   than in the base rule. */
button.avatar-profile{appearance:none; border:none; padding:0; cursor:pointer; font-family:inherit;}
button.avatar-profile:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:4px;}
/* No ground under *this* avatar, unlike every other one: it sits on the page
   itself rather than on a card, so the transparent styles take the page's
   background and tile texture and read as drawn on it. The accent gradient
   this used to carry is what ruled that out before -- it gave the transparent
   styles a halo the opaque ones never got from the same picker. */
.avatar-profile{background:none;}
.avatar-profile > img{border-radius:50%;}
.avatar-profile .edit-badge{
  position:absolute; bottom:0; left:0; width:26px; height:26px; border-radius:50%;
  background:var(--bg-1); border:2px solid var(--bg-0); display:grid; place-items:center; color:var(--text-1);
}
.avatar-profile .edit-badge svg{width:12px; height:12px;}
/* the badge is the only thing saying the picture is tappable, so it takes
   the accent when it actually is */
button.avatar-profile .edit-badge{background:var(--fayrouzeh); color:var(--on-accent);}
.profile-head h1{font-size:var(--fs-xl); font-weight:800; margin:0 0 var(--space-2);}
.profile-head p{font-size:var(--fs-md); color:var(--text-3); margin:0;}

/* --- profile: «اطلاعات حساب» --- */
/* Reuses the plan card's label/value rows (.fact-list.boxed) rather than a
   second look for the same shape. An unset optional field keeps its row and
   goes quiet, so it still reads as something you may fill in. */
.profile-info{padding:0 var(--space-10) var(--space-9);}
.profile-info .section-head{padding:var(--space-2) var(--space-1) var(--space-5);}
.profile-info .fact dd{max-width:62%; text-align:end; line-height:1.7;}
.profile-info .fact dd.is-empty{color:var(--text-3); font-weight:500;}
.profile-info .info-note{font-size:var(--fs-sm); color:var(--text-3); line-height:1.7; margin:var(--space-4) var(--space-1) 0;}

/* An operator reading someone else's profile. The tint is the accent's --
   this is the app speaking about who you are right now, which is the accent
   channel, not a status and not a category, so neither of those two hues may
   appear here. It is a tinted fill over `--accent-rgb` rather than a second
   copy of a colour, so both themes are covered by the token they already
   ship. */
.profile-note{padding:0 var(--space-10); margin:0 0 var(--space-7);}
.profile-note span{
  display:block; padding:var(--space-5) var(--space-7); border-radius:var(--radius-md);
  background:rgba(var(--accent-rgb),.10);
  font-size:var(--fs-sm); line-height:1.8; color:var(--text-2);
}
/* The role row on a profile: the settings screen's own group primitive,
   given this page's horizontal padding. It borrows `.setting-group` rather
   than growing a look of its own -- a labelled row showing its current value
   is exactly what that control already is. */
.profile-admin{padding:0 var(--space-10) var(--space-9);}

.menu-list{display:flex; flex-direction:column; gap:var(--space-5); padding:0 var(--space-10);}
.menu-item{
  display:flex; align-items:center; gap:var(--space-6); padding:var(--space-7) var(--space-8); cursor:pointer;
}
.menu-item .mi-icon{
  width:36px; height:36px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center; background:rgba(var(--accent-rgb),0.1); color:var(--fayrouzeh);
}
.menu-item .mi-icon svg{width:17px; height:17px;}
.menu-item.danger .mi-icon{background:rgba(var(--mohr-rgb),0.12); color:var(--danger-text);}
.menu-item span.mi-label{flex:1; font-size:var(--fs-lg); font-weight:600;}
/* current value of a row that *has* one (the theme control) -- it sits
   between the label and the chevron, so :empty must collapse it or the
   chevron shifts once JS fills it in */
.menu-item .mi-value{font-size:var(--fs-md); color:var(--text-3); flex-shrink:0;}
.menu-item .mi-value:empty{display:none;}
.menu-item .mi-chev{width:16px; height:16px; color:var(--text-3);}

/* ==========================================================================
   Settings  (/views/settings/)
   ==========================================================================
   Grouped rows, not a flat menu: a preference screen is scanned for a
   heading first, so the group title does the finding and the row only has to
   be readable once found. Colour is deliberately absent -- nothing here is a
   status or a category, and the two channels those own must not leak into a
   surface that means neither. The one accent on the page is the segmented
   control's selected option, which is the *answer* to the row. */
.settings-page{padding:0 var(--space-10) var(--space-4); display:flex; flex-direction:column; gap:var(--space-11);}
.setting-group{display:flex; flex-direction:column; gap:var(--space-5);}
.setting-group > h2{
  font-size:var(--fs-md); font-weight:700; color:var(--text-3); margin:0 var(--space-1);
  letter-spacing:.2px;
}
.setting-row{
  display:block; padding:var(--space-7) var(--space-8); text-decoration:none; color:inherit;
}
a.setting-row, .setting-row[role="button"]{cursor:pointer;}
.sr-head{display:flex; align-items:center; gap:var(--space-5);}
.sr-text{display:flex; flex-direction:column; gap:var(--space-2); min-width:0; flex:1;}
.sr-label{font-size:var(--fs-lg); font-weight:600;}
/* what changing this does, in one line -- a setting whose effect can't be
   predicted is a setting nobody dares touch */
.sr-hint{font-size:var(--fs-sm); color:var(--text-3); line-height:1.7;}
.sr-value{font-size:var(--fs-md); color:var(--text-2); flex-shrink:0;}
.sr-value.muted{color:var(--text-3);}
.sr-value:empty{display:none;}
.sr-chev{width:16px; height:16px; color:var(--text-3); flex-shrink:0;}
/* the options sit under the row's own text, so the label reads as the
   question and the control as its answer */
.setting-row .seg{margin-top:var(--space-6);}
/* The switch -- the settings screen's yes/no control (notification kinds
   today). A real `<input type=checkbox>` does the work: it is focusable,
   keyboard-operable and announced without a line of JS, and the whole
   `.sr-head` is its `<label>`, so tapping anywhere on the row flips it. The
   input is hidden from sight but never from the accessibility tree or the
   tab order -- `opacity:0` over the track rather than `display:none`, which
   would take it out of both.

   `--fayrouzeh` (the primary accent) for on, `--track` for off, and the knob's travel is mirrored by
   `dir` on purpose: in RTL "on" reads to the left, and using a logical
   `inset-inline-start` for the knob makes that fall out of the direction
   rather than out of a second rule. Colour is not the only mark -- the knob
   moves, which is what carries the state where colour cannot. */
.switch{position:relative; flex-shrink:0; width:44px; height:26px;}
.switch input{
  position:absolute; inset:0; width:100%; height:100%;
  margin:0; opacity:0; cursor:pointer; z-index:1;
}
.switch-track{
  display:block; position:relative; width:100%; height:100%; border-radius:var(--radius-pill);
  background:var(--track); border:1px solid var(--border);
  transition:background var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
.switch-knob{
  position:absolute; top:3px; inset-inline-start:3px;
  width:18px; height:18px; border-radius:50%;
  background:var(--bg-1); box-shadow:0 1px 3px rgba(0,0,0,.28);
  transition:translate var(--dur-base) var(--ease), background var(--dur-base) var(--ease);
}
.switch input:checked + .switch-track{
  background:var(--fayrouzeh); border-color:transparent;
  box-shadow:var(--glow-accent);
}
.switch input:checked + .switch-track .switch-knob{
  translate:calc(18px * var(--switch-dir, 1)) 0;
  background:var(--on-accent);
}
[dir="rtl"] .switch{--switch-dir:-1;}
.switch input:focus-visible + .switch-track{
  outline:2px solid var(--fayrouzeh); outline-offset:2px;
}
/* Mid-flight: the row is showing what the server has not confirmed yet, so
   it says so rather than looking settled. */
.switch.is-saving{opacity:.55;}
/* The notification rows are labels wrapping a control, so the label must lay
   out like the `.sr-head` it replaces and must not inherit the pointer-less
   look of a static row. */
label.sr-head{cursor:pointer; width:100%;}
.settings-note{font-size:var(--fs-sm); color:var(--text-3); margin:var(--space-1) var(--space-1) 0; text-align:center;}

/* --- Admin panel (app/templates/admin/index.html) ---
   Built out of the settings screen's own primitives (.settings-page,
   .setting-group, .setting-row) rather than a layout of its own: it is the
   same shape of question -- a list of rows, each with a label, a hint and
   its current value on the row -- and an admin-only screen is the last place
   that should be maintained as a separate design.

   The role pill is neutral by default and accent only for `admin`. It is
   *not* a status or a category, so it takes neither channel's hues (see the
   colour rules above); the accent here means "this one is the exception",
   which is the one thing the list is scanned for. */
.admin-stats{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5);}
.admin-stat{padding:var(--space-7) var(--space-5); display:flex; flex-direction:column; align-items:center; gap:var(--space-2);}
.as-value{font-size:var(--fs-2xl); font-weight:800;}
.as-label{font-size:var(--fs-sm); color:var(--text-3);}
.admin-list{display:flex; flex-direction:column; gap:var(--space-5);}
/* An opaque ground under the picture: several catalogue avatars are
   transparent, so anything translucent here would let the card show through
   and give those members a halo the others never get. */
.pill-role{color:var(--text-2); flex-shrink:0; border:0;}
.pill-role.is-admin{background:rgba(var(--accent-rgb),.14); color:var(--fayrouzeh);}

/* The roster's search box and role segment are the challenge list's own
   controls, reused verbatim so the app has one way to filter a list rather
   than two. Both were built for a full-bleed page and carry the 20px page
   gutter themselves; inside `.settings-page` that gutter is already there,
   and `.setting-group`'s own gap already spaces them -- so the inset and the
   trailing space are cancelled here, on this one screen, instead of the
   shared rules being loosened for it. */
.admin-members .search-bar{margin:0;}
.admin-members .seg-row{padding:0;}
/* An empty state inside a group, not a whole page: the stats above it stay
   put, so it does not need the full-page block's height. */
.admin-members .empty{padding:32px var(--space-12);}

/* The moderation roster's rows (admin/_challenge_rows.html). A challenge row
   is a member row with one extra mark: the category tile, which is the only
   place the taxonomy channel appears on this screen -- it reads --cat from
   its own data-cat, with the accent fallback every other [data-cat] surface
   has, so a row missing the attribute still renders. */
.admin-cat{
  width:34px; height:34px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:color-mix(in srgb, var(--cat, var(--fayrouzeh)) 16%, transparent);
  color:var(--cat, var(--fayrouzeh));
}
.admin-cat svg{width:17px; height:17px;}
/* The panel's three counts are links into the lists they count, so they take
   the shared press feedback and a hover lift like every other tappable card.
   `text-decoration:none` and `color:inherit` are on `.setting-row`, not
   here, so they are restated for these. */
a.admin-stat{text-decoration:none; color:inherit;}

/* The participants drill-down on a moderation row. A second line rather than
   a fourth item in `.sr-head`: a 34px tile, a two-line title, a status pill
   and a chevron already fill the row's width on a phone, and squeezing a
   fifth control in is how the title starts truncating. The hairline is what
   makes it read as its own target instead of a sentence someone wrote. */
.sr-foot{margin-top:var(--space-5); padding-top:var(--space-5); border-top:1px solid var(--border);}
.admin-members-link{
  display:flex; align-items:center; gap:var(--space-4);
  font-size:var(--fs-md); font-weight:600; color:var(--fayrouzeh);
  text-decoration:none;
}
.admin-members-link svg{width:15px; height:15px;}
/* The chevron is pushed to the far end so the link spans the row and the
   whole line is the target, not just the words. */
.admin-members-link .sr-chev{margin-inline-start:auto; color:inherit;}
/* A read-only row: nothing to open, so it must not offer a pointer or the
   press squeeze the actionable rows use (see admin/_participant_rows.html
   for why removal is not a moderation power). */
.admin-participant{cursor:default;}

/* ==========================================================================
   Notifications  (/views/notifications/)
   ==========================================================================
   The feed reuses the settings screen's primitives (.settings-page,
   .setting-group, .setting-row) rather than owning a layout: it is the same
   shape of question -- a column of rows, each a label, a line of detail and a
   mark -- and a third variant of that column would be a third thing to keep
   in step with the other two.

   Colour here means exactly one thing: **unread**. A notification is neither
   a status nor a category, so it takes neither of those two channels' hues
   (see the colour rules at the top of this file); the accent is spent on the
   one distinction the list is actually scanned for, and it is spelled out by
   the «تازه» pill as well, because colour alone may not carry a meaning. */
.notif-list{display:flex; flex-direction:column; gap:var(--space-5);}
/* A neutral tile by default -- the glyph says *what kind* of event this was,
   which is a label, not a signal. It lights up only while the row is unread,
   so the accent enters and leaves with the one meaning it has here. */
.notif-icon{
  width:34px; height:34px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:var(--fill-1); color:var(--text-2);
  transition:background var(--dur-base) var(--ease), color var(--dur-base) var(--ease);
}
.notif-icon svg{width:17px; height:17px;}
/* Three marks for one state, none of them load-bearing alone: a warmer
   border, the lit tile, and the pill that says the word. */
.notif-row.is-unread{border-color:rgba(var(--accent-rgb),.34);}
.notif-row.is-unread .notif-icon{
  background:rgba(var(--accent-rgb),.14); color:var(--fayrouzeh);
}
.notif-row.is-unread .sr-label{font-weight:700;}
.notif-new{
  font-size:var(--fs-xs); font-weight:700; padding:var(--space-2) var(--space-4); border-radius:var(--radius-pill);
  background:rgba(var(--accent-rgb),.14); color:var(--fayrouzeh); flex-shrink:0;
}
/* When it happened, under the sentence rather than beside it: the row already
   carries a tile, two lines of text, a pill and a chevron, and a fifth item
   in `.sr-head` is what starts truncating the sentence on a phone. */
.notif-time{font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}
/* A row whose kind names no challenge has nowhere to go, so it must not
   offer a pointer or the press squeeze the tappable rows use. */
.notif-row.is-static{cursor:default;}
/* An empty feed is the good case, not a failure -- it gets room to say so. */
.notif-feed .empty{padding:44px var(--space-12);}
/* ==========================================================================
   Jalali date / datetime picker  (app/static/js/jalali-picker.js)
   ==========================================================================
   The native <input type="date"> stays in the DOM as the value holder but is
   taken out of the visual and tab flow; `.jp-trigger` is what the user sees
   and operates. Not `display:none` -- a hidden input is skipped by some form
   validation paths, and the element still has to be reachable to scripts. */
.jp-native{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}
.jp-trigger{
  flex:1; min-width:0; text-align:start; cursor:pointer;
  border:none; background:transparent; outline:none;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); padding:var(--space-6) var(--space-1);
}
.jp-trigger.is-empty{color:var(--text-3);}

.jp-panel{max-width:min(var(--shell-max), 420px);}

.jp-nav{display:flex; align-items:center; gap:var(--space-2); margin-bottom:var(--space-6);}
.btn-nav{
  width:34px; height:34px; flex-shrink:0;
  border-radius:var(--radius-sm); border:1px solid var(--border);
  background:var(--glass); color:var(--text-2);
  transition:background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.btn-nav:hover{background:var(--glass-strong); color:var(--text-1);}
.btn-nav svg{width:15px; height:15px;}
/* the year chevrons are a stacked pair, so pull them together */
.jp-prev-year svg, .jp-next-year svg{margin-inline:-4px;}

.jp-title{
  flex:1; padding:var(--space-4); border:none; background:transparent; cursor:pointer;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); font-weight:800;
  border-radius:var(--radius-sm); transition:background var(--dur-fast) var(--ease);
}
.jp-title:hover{background:var(--glass);}

.jp-weekdays,
.jp-grid{display:grid; grid-template-columns:repeat(7,1fr); gap:var(--space-2);}
.jp-weekdays{
  margin-bottom:var(--space-3); font-size:var(--fs-sm); font-weight:700; color:var(--text-3);
  text-align:center;
}
.jp-cell{
  aspect-ratio:1; display:flex; align-items:center; justify-content:center;
  border-radius:var(--radius-sm); border:1px solid transparent;
  background:var(--glass); color:var(--text-1);
  font-family:inherit; font-size:var(--fs-lg); cursor:pointer;
  transition:background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.jp-cell:hover:not(:disabled){background:var(--glass-strong);}
.jp-cell:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:1px;}
.jp-blank{background:transparent; border:none; cursor:default;}
.jp-cell.is-holiday{color:var(--mohr);}
.jp-cell.is-today{border-color:var(--border-strong); font-weight:800;}
.jp-cell.is-selected{
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  border-color:transparent; color:var(--on-accent); font-weight:800;
}
.jp-cell:disabled{opacity:.28; cursor:not-allowed;}

.jp-time{
  display:flex; align-items:center; gap:var(--space-4); margin-top:var(--space-7);
  background:var(--fill-1); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:var(--space-4) var(--space-6);
}
.jp-time svg{width:16px; height:16px; color:var(--text-3); flex-shrink:0;}
.jp-time-label{font-size:var(--fs-md); color:var(--text-2); font-weight:600;}
.jp-select{
  border:none; background:transparent; outline:none; cursor:pointer;
  color:var(--text-1); font-family:inherit; font-size:var(--fs-lg); font-weight:700;
  padding:var(--space-2) var(--space-1);
}
.jp-select option{background:var(--bg-2); color:var(--text-1);}
.jp-colon{color:var(--text-3); font-weight:700;}
.jp-time .jp-select:first-of-type{margin-inline-start:auto;}

.jp-actions .jp-clear{flex:0 0 auto; color:var(--text-3);}

/* ==========================================================================
   Challenge detail | cadence plan, details grid, personal status
   ========================================================================== */

.detail-tags{
  position:relative; z-index:1;
  display:flex; flex-wrap:wrap; gap:var(--space-3); justify-content:center; margin-bottom:var(--space-5);
}
/* the accent tag names the cadence -- the one label that changes per
   challenge type, so it gets the filled treatment the category tag doesn't */
.pill-tag.accent{background:rgba(var(--accent-rgb),0.22); border-color:rgba(var(--accent-rgb),0.5);}
.pill-tag.warn{
  color:var(--danger-text); background:rgba(var(--mohr-rgb),0.16); border-color:rgba(var(--mohr-rgb),0.42);
}

.section-head{display:flex; align-items:center; justify-content:space-between; gap:var(--space-5); margin-bottom:var(--space-5);}
.section-head h2{margin:0;}
.section-head .pill{
  font-weight:600; direction:ltr; font-size:var(--fs-xs); color:var(--text-3);
  background:var(--fill-2); border-color:var(--border);
  max-width:55%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.sub-head{font-size:var(--fs-md); font-weight:700; color:var(--text-2); margin:var(--space-7) 0 var(--space-4);}
.progress-note{font-size:var(--fs-sm); color:var(--text-3) !important; margin:var(--space-4) 0 0 !important;}

/* --- the plan card: what this cadence actually asks of you --- */
.plan-card{padding:var(--space-7); border-radius:var(--radius-md);}
.plan-rule{display:flex; align-items:flex-start; gap:var(--space-6);}
.plan-rule-icon{
  width:34px; height:34px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:rgba(var(--accent-rgb),0.14); border:1px solid rgba(var(--accent-rgb),0.28);
}
.plan-rule-icon svg{width:17px; height:17px; color:var(--fayrouzeh);}
.plan-rule-text{min-width:0;}
.plan-rule-text b{display:block; font-size:var(--fs-lg); font-weight:700; line-height:1.7;}
.plan-rule-text span{display:block; font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}

.weekday-strip{display:grid; grid-template-columns:repeat(7,1fr); gap:var(--space-3); margin-top:var(--space-6);}
.wd{
  height:30px; border-radius:var(--radius-sm); display:grid; place-items:center;
  font-size:var(--fs-sm); font-weight:700; color:var(--text-3);
  background:var(--fill-1); border:1px solid var(--border);
}
.wd.on{
  color:var(--on-accent); border-color:transparent;
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
}

.plan-progress{margin-top:var(--space-6);}
.plan-progress span{display:block; font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-3);}

/* --- label/value rows, shared by the plan card and "وضعیت من" --- */
.fact-list{margin:0; padding:0;}
.plan-card .fact-list{margin-top:var(--space-6); border-top:1px solid var(--border); padding-top:var(--space-2);}
.fact{
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-6);
  padding:var(--space-4) 0; border-bottom:1px solid var(--border);
}
.fact:last-child{border-bottom:none;}
.fact dt{font-size:var(--fs-sm); color:var(--text-3); margin:0;}
.fact dd{font-size:var(--fs-md); font-weight:600; color:var(--text-1); margin:0; text-align:end;}

/* --- what's coming up --- */
.up-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--space-3);}
.up-row{
  display:flex; align-items:center; gap:var(--space-5);
  padding:var(--space-5) var(--space-6); border-radius:var(--radius-sm);
  background:var(--glass); border:1px solid var(--border);
}
.up-row.next{background:rgba(var(--accent-rgb),0.08); border-color:rgba(var(--accent-rgb),0.28);}
.up-dot{
  width:7px; height:7px; border-radius:50%; flex-shrink:0;
  background:var(--text-3);
}
.up-row.next .up-dot{background:var(--fayrouzeh); box-shadow:0 0 0 3px rgba(var(--accent-rgb),0.18), var(--glow-accent);}
.up-date{font-size:var(--fs-md); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.up-time{font-size:var(--fs-sm); color:var(--text-2); font-variant-numeric:tabular-nums;}
.up-flag{
  margin-inline-start:auto; font-size:var(--fs-xs); font-weight:700;
  color:var(--fayrouzeh); background:rgba(var(--accent-rgb),0.14);
  border-radius:var(--radius-pill); padding:var(--space-1) var(--space-4);
}

/* --- details grid --- */
.meta-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:var(--space-4);}
.meta-item{
  display:flex; align-items:center; gap:var(--space-5); min-width:0;
  padding:var(--space-6) var(--space-6); border-radius:var(--radius-sm);
  background:var(--glass); border:1px solid var(--border);
}
.meta-item > svg, .meta-item > span > svg{width:15px; height:15px; color:var(--fayrouzeh); flex-shrink:0;}
.meta-item > div{min-width:0;}
.meta-item b{
  display:block; font-size:var(--fs-md); font-weight:700;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.meta-item span{font-size:var(--fs-xs); color:var(--text-3);}

/* --- personal status tiles --- */
/* three tiles now, not four: the two completion counts moved into the ring
   legend above, where they read as shares of the same whole. */
.mine-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-4); margin-bottom:var(--space-6);}
.mine-tile{
  padding:var(--space-6) var(--space-3); text-align:center; border-radius:var(--radius-sm);
  background:var(--glass); border:1px solid var(--border);
}
.mine-tile b{display:block; font-size:var(--fs-xl); font-weight:800; line-height:1.2; white-space:nowrap;}
.mine-tile b .dim{display:inline; color:var(--text-3); font-weight:600; font-size:var(--fs-lg);}
.mine-tile span{display:block; font-size:var(--fs-xs); color:var(--text-3); margin-top:var(--space-2);}
.mine-tile.ok b{color:var(--fayrouzeh);}
.mine-tile.gold b{
  color:var(--gold); display:flex; align-items:center; justify-content:center; gap:var(--space-2);
}
.mine-tile.gold b svg{width:13px; height:13px;}


/* ==========================================================================
   Challenge status: «چراغ وضعیت», and the filter shell around it
   ==========================================================================
   Status used to be painted onto the card four times over: a coloured edge
   stripe, a status-tinted border, a dimmed «تمام‌شده» card, and a pill
   spelling the word out. Four marks for one derived fact, on a surface that
   already carries a category hue, a cover photograph, a cadence badge and
   two social controls -- which is how every card came to read as the same
   card. All of it is gone.

   The whole channel is now one object, the lamp below, and the only other
   consumer of --st-* is the filter row that selects by status. Nothing here
   touches how the status is *derived*: `challenge_status` and `status_filter`
   are untouched, and this is presentation only. -------------------------- */

/* --- the lamp: one component, one markup, three views --------------------
   A dot the colour of the status, which says the word when asked. Closed at
   rest, because a list of cards is scanned rather than read and three spelt
   out statuses down a column is the pill row this replaced; open on a tap
   (for two seconds, then it puts itself away) or, where there is a real
   pointer, for as long as it is hovered.

   The open width comes from `grid-template-columns:auto 0fr -> auto 1fr`,
   never from a `max-width`. A max-width has to be guessed, so it is either
   short of the longest label -- which then truncates -- or longer than the
   shortest, which then finishes collapsing well after the animation looks
   over. `0fr` measures the label itself, so all three statuses open and shut
   at the same rate whatever their word.

   The lamp runs `direction:ltr` inside the RTL page on purpose: the ring is
   the thing anchored to a corner, and with the ring at the box's physical
   left the label grows *into* the card rather than off its edge, whichever
   surface it is riding on.

   Colour is the status channel and nothing else: --status-rgb is set by the
   slug class and read by the ring, its fill and the open ground. */
.st-lamp{
  --lamp-dur:.26s;
  --lamp-ease:cubic-bezier(.32,.72,0,1);
  direction:ltr;
  display:inline-grid; grid-auto-flow:column; align-items:center;
  grid-template-columns:auto 0fr; gap:0;
  height:24px; padding:0; border:0; border-radius:var(--radius-pill);
  background:transparent; color:rgb(var(--status-rgb));
  font-family:inherit; font-size:var(--fs-xs); font-weight:700; line-height:1;
  flex-shrink:0; cursor:pointer; -webkit-tap-highlight-color:transparent;
  transition:grid-template-columns var(--lamp-dur) var(--lamp-ease),
             gap var(--lamp-dur) var(--lamp-ease),
             padding var(--lamp-dur) var(--lamp-ease),
             background var(--lamp-dur) var(--lamp-ease);
}
.st-lamp.active{--status-rgb:var(--st-active-rgb);}
.st-lamp.upcoming{--status-rgb:var(--st-upcoming-rgb);}
.st-lamp.finished{--status-rgb:var(--st-finished-rgb);}
/* A ring around a filled dot rather than a bare dot: the hairline is what
   keeps it visible against a cover photograph of roughly its own colour. */
.st-lamp .ring{
  width:14px; height:14px; border-radius:50%; flex-shrink:0;
  border:1.5px solid rgba(var(--status-rgb),.42);
  display:grid; place-items:center;
}
.st-lamp .ring::before{
  content:""; width:6px; height:6px; border-radius:50%;
  background:rgb(var(--status-rgb));
}
/* The word is Farsi, so it keeps the page's own direction inside the ltr
   box. It slides the last few pixels in rather than appearing, or the text
   arrives before the room it needs has finished opening. */
.st-lamp .label{
  direction:rtl; overflow:hidden; white-space:nowrap;
  opacity:0; transform:translateX(4px);
  transition:opacity .2s var(--lamp-ease), transform .2s var(--lamp-ease);
}
.st-lamp.is-open{
  grid-template-columns:auto 1fr; gap:6px;
  padding:0 9px 0 5px;
  background:rgba(var(--status-rgb),.12);
}
.st-lamp.is-open .label{opacity:1; transform:none;}
.st-lamp:focus-visible{
  outline:2px solid rgb(var(--status-rgb)); outline-offset:2px;
}
/* Somebody who asked for less motion still gets the label -- the width is
   what carries it -- just without the sweep. */
@media (prefers-reduced-motion:reduce){
  .st-lamp{--lamp-dur:1ms;}
  .st-lamp .label{transition-duration:1ms;}
}

/* THE LIST ROW. Exactly where the pill was -- the first line of the column
   at the card's outer edge -- but out of flow, with its closed height
   reserved as padding above the actions. In flow, opening it would widen
   that column and re-ellipsis the title beside it; absolute, the card's
   geometry cannot move at all. It overlaps the title's tail while open, so
   here the open ground is opaque: a 12% tint over glass is not something a
   line of text can be read through. */
.ec-side{position:relative; padding-block-start:calc(24px + var(--space-3));}
.ec-side > .st-lamp{position:absolute; top:0; left:0; z-index:2;}
.ec-side > .st-lamp.is-open{
  background:
    linear-gradient(rgba(var(--status-rgb),.12), rgba(var(--status-rgb),.12)),
    var(--bg-2);
}

.explore-head{display:flex; align-items:flex-start; gap:var(--space-4); margin-bottom:var(--space-2);}
.explore-head h3{margin:0; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}

/* The status buttons in the filter control wear the colour of the thing they
   select, so the legend and the filter are the same object. */
.btn-seg.is-active.active{background:var(--st-active); color:var(--on-accent); box-shadow:var(--glow-accent);}
.btn-seg.is-upcoming.active{background:var(--st-upcoming); color:var(--on-gold); box-shadow:var(--glow-gold);}
.btn-seg.is-finished.active{background:var(--st-finished); color:var(--on-finished); box-shadow:none;}

/* ==========================================================================
   Category colour — the taxonomy channel
   ==========================================================================
   Status answers "should I act on this" and owns the --st-* hues, which
   since «چراغ وضعیت» land on exactly two things: the lamp, and the filter
   button that selects by status. Category answers "what kind of thing is
   this" and owns the --cat-* hues, and the two never share a surface: the
   hue lands on the thumb/hero tile and on the word naming the category,
   never on the lamp. Everything reads it from one custom property the card
   sets from its `data-cat`, so a template that forgets the attribute simply
   falls back to the accent styling it had before.
   The slug is `ChallengeCategory.name | lower` (see the four card templates);
   adding a category means a hue in all three theme blocks plus one line here.
   ========================================================================== */
[data-cat="fitness"]{--cat:var(--cat-fitness); --cat-rgb:var(--cat-fitness-rgb);}
[data-cat="nutrition"]{--cat:var(--cat-nutrition); --cat-rgb:var(--cat-nutrition-rgb);}
[data-cat="mental_health"]{--cat:var(--cat-mental); --cat-rgb:var(--cat-mental-rgb);}
[data-cat="productivity"]{--cat:var(--cat-productivity); --cat-rgb:var(--cat-productivity-rgb);}
[data-cat="social_responsibility"]{--cat:var(--cat-social); --cat-rgb:var(--cat-social-rgb);}
[data-cat="other"]{--cat:var(--cat-other); --cat-rgb:var(--cat-other-rgb);}

/* the explore card's thumb: tinted fill, hairline and icon, all one hue */
[data-cat] .explore-thumb{
  background:linear-gradient(145deg, rgba(var(--cat-rgb),0.13), rgba(var(--cat-rgb),0));
  box-shadow:inset 0 0 0 1px rgba(var(--cat-rgb),0.20);
  color:var(--cat);
}
/* the category word in the card's meta line — the label and the tile agree,
   so the colour is legible as *meaning* and not as decoration */
[data-cat] .cat-name{color:var(--cat); font-weight:700;}

/* the category chip on the home / today / detail cards. .pill-tag is the
   accent chip; .pill-cat re-points the same shape at the category hue. */
.pill-cat{
  color:var(--cat, var(--fayrouzeh));
  background:rgba(var(--cat-rgb, var(--accent-rgb)),0.12);
  border-color:rgba(var(--cat-rgb, var(--accent-rgb)),0.28);
}

/* the detail hero's filled tile, plus the wash behind it */
[data-cat] .detail-icon{
  background:linear-gradient(145deg, var(--cat), rgba(var(--cat-rgb),0.72));
  box-shadow:0 10px 30px rgba(var(--cat-rgb),0.26);
}
[data-cat] .detail-icon svg{color:var(--on-cat);}
/* the corner badge sits *outside* the filled tile's ink: it keeps the card's
   own ground and wears the category hue, so it has to win the line above
   (same specificity, so it has to come after it) */
[data-cat] .detail-icon .badge-cadence{color:var(--cat);}
[data-cat] .detail-icon .badge-cadence svg{color:inherit;}
[data-cat].detail-hero::before{
  background:radial-gradient(60% 80% at 50% -10%, rgba(var(--cat-rgb),0.18), transparent 70%);
}

/* Four segments of Farsi don't fit a 480px shell at the .btn-seg base size,
   and a filter that has to be scrolled to be discovered isn't a filter. */
#clStatusSeg .btn-seg{padding:var(--space-4) var(--space-2); font-size:var(--fs-sm); gap:var(--space-2);}
#clStatusSeg .btn-seg svg{width:13px; height:13px;}

/* --- filter button: never let a filter be on invisibly --- */
.btn-filter{position:relative;}
.btn-filter.has-filters{
  color:var(--fayrouzeh); border-color:rgba(var(--accent-rgb),0.45);
  background:rgba(var(--accent-rgb),0.12);
}
.filter-badge{
  position:absolute; inset-block-start:-6px; inset-inline-start:-6px;
  min-width:16px; height:16px; padding:0 var(--space-2); border-radius:var(--radius-pill);
  display:grid; place-items:center;
  background:var(--fayrouzeh); color:var(--on-accent);
  font-size:var(--fs-xs); font-weight:800; line-height:1;
  border:2px solid var(--bg-0);
}
/* display:grid above would otherwise beat the UA's [hidden]{display:none}. */
.filter-badge[hidden]{display:none;}

/* --- what the hidden filters are doing right now --- */
.active-filters[hidden]{display:none;} /* see .crumbs[hidden] above */
.active-filters{display:flex; flex-wrap:wrap; gap:var(--space-4); padding:0 var(--space-10) var(--space-7);}
.filter-tag{
  display:inline-flex; align-items:center; gap:var(--space-3);
  padding:var(--space-3) var(--space-6); border-radius:var(--radius-pill); cursor:pointer;
  font-family:inherit; font-size:var(--fs-sm); font-weight:600;
  background:rgba(var(--accent-rgb),0.12); border:1px solid rgba(var(--accent-rgb),0.34);
  color:var(--fayrouzeh);
}
.filter-tag svg{width:11px; height:11px;}
.filter-tag:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px;}
.filter-tag.clear{
  background:transparent; border-color:var(--border); color:var(--text-3);
}

/* --- chips and avatars inside a sheet --- */
/* Both are their own grid of controls, so they drop the boxed input shell
   that wraps every other field type. */
.field-chips .field-input, .field-avatars .field-input{
  display:block; background:transparent; border:none; padding:0;
}
.sheet-chips{padding:0; gap:var(--space-4);}

/* --- «انتخاب عکس» ---------------------------------------------------
   One control wherever a picture is chosen: the create wizard's two fields,
   the manage sheet's two, and (as `.ap-upload`) the first tile of the avatar
   grid. The preview is drawn *at the frame's own aspect*, so the shape is the
   explanation and no label has to say "square" twice. */
.field-image .field-input{display:block; background:transparent; border:none; padding:0;}
.ipf-pair{display:grid; grid-template-columns:1fr 1fr; gap:var(--space-6); align-items:start;}
.ipf{display:flex; flex-direction:column; gap:var(--space-4);}
.ipf .field-note{margin:0;}
.ipf-preview{
  position:relative; overflow:hidden;
  border-radius:var(--radius-md); background:var(--fill-1);
  box-shadow:inset 0 0 0 1px var(--border);
  display:grid; place-items:center; color:var(--text-3);
}
.ipf-preview.is-square{aspect-ratio:1/1;}
.ipf-preview.is-tall{aspect-ratio:9/16;}
.ipf-preview img{width:100%; height:100%; object-fit:cover; display:block;}
.ipf-preview svg{width:26px; height:26px; opacity:.7;}
/* Inside a sheet the picture is one row among six, so it lies down: a
   full-width preview there pushes «ذخیره» off the screen and makes one
   optional field look like the form. In the wizard, where the two frames
   *are* the question, they stay upright and side by side. */
.field-image .ipf{flex-direction:row; align-items:center;}
.field-image .ipf-preview{flex:0 0 84px; width:84px;}
.field-image .ipf-acts{flex:1; flex-direction:column; align-items:stretch;}
.ipf-acts{display:flex; gap:var(--space-4);}
.ipf-acts button{flex:1; white-space:nowrap;}
.ipf-clear[hidden]{display:none;}
/* the destructive half of the pair stays neutral while the affirmative half
   carries the accent -- the same rule the manage screen's rows follow */
.ipf-clear{color:var(--text-3);}

/* A picture of one's own, first in the avatar grid: the answer most people
   are looking for when they tap their avatar. It is a tile like the forty
   beside it, marked out by the accent rather than by a different shape. */
.ap-item.ap-upload{
  display:grid; place-items:center;
  color:var(--fayrouzeh);
  background:rgba(var(--accent-rgb),0.10);
  box-shadow:inset 0 0 0 1px rgba(var(--accent-rgb),0.30);
}
.ap-item.ap-upload svg{width:22px; height:22px;}

/* --- the crop step ------------------------------------------------------
   Full-screen, above every sheet: it *is* the task while it is open, and a
   crop box the size of a form field cannot be aimed with a thumb. It borrows
   `createSheet`'s own scrim and panel so it reads as one more of this app's
   sheets rather than as the library's own furniture. */
.ip-modal{position:fixed; inset:0; z-index:calc(var(--z-sheet) + 4); display:flex; align-items:flex-end;}
.ip-modal .sheet-backdrop{z-index:0;}
.ip-panel{
  position:relative; z-index:1;
  width:100%; max-width:var(--shell-max); margin-inline:auto;
  border-radius:var(--radius-lg) var(--radius-lg) 0 0; padding:var(--space-8) var(--space-8) calc(var(--space-8) + env(safe-area-inset-bottom));
  max-height:92dvh; display:flex; flex-direction:column;
  animation:sheetSlideUp var(--dur-base) var(--ease);
}
.ip-hint{margin:0 0 var(--space-5); font-size:var(--fs-md); color:var(--text-3); line-height:1.9;}
.ip-stage{
  flex:1; min-height:0; border-radius:var(--radius-md); overflow:hidden;
  background:var(--fill-1); margin-bottom:var(--space-7);
}
/* Cropper measures the source image, then builds its own canvas out of
   copies of it. The child combinator is load-bearing: a bare `.ip-stage img`
   also hits those copies and overrides the sizes the library just computed,
   which leaves a correctly-built cropper drawing nothing. */
.ip-stage > img{display:block; max-width:100%; height:52dvh;}
/* The library ships its own greys; these repoint the three parts that show
   through to the app's own tokens, so the crop reads in both themes. */
.ip-stage .cropper-view-box{outline-color:var(--fayrouzeh);}
.ip-stage .cropper-line, .ip-stage .cropper-point{background-color:var(--fayrouzeh);}
.ip-stage .cropper-modal{background-color:#000; opacity:.55;}

/* ==========================================================================
   Challenge detail | panels, personal ring, occurrence strip
   ========================================================================== */

/* --- panel switcher ---
   The detail page answers three separate questions and used to stack all
   three answers into one nine-section scroll. Tabs let a participant land on
   the one they came for and never meet the other two unless they ask.
   Sticky, because "which panel am I in" has to survive scrolling a long
   history. Same pill vocabulary as .tabs on the home page, but full-width
   and icon-led -- these switch a whole panel, not a filter. */
.detail-tabs{
  /* the topbar is sticky too (z-index 25), so this has to stop *below* it
     rather than slide underneath. Its height depends on how the challenge
     title wraps, and on desktop it parks under the top nav rather than at 0
     -- so the page script measures where its bottom edge lands (parked
     offset + height) into --topbar-h. */
  position:sticky; top:var(--topbar-h, 0px); z-index:var(--z-sticky);
  display:flex; gap:var(--space-3); margin:0 var(--space-10) var(--space-7); padding:var(--space-2);
  border-radius:var(--radius-pill);
  background:var(--chrome-bg-strong); border:1px solid var(--border);
  backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
}
.dtab{
  flex:1; position:relative;
  display:flex; align-items:center; justify-content:center; gap:var(--space-3);
  padding:var(--space-5) var(--space-3); border:none; border-radius:var(--radius-pill); cursor:pointer;
  background:transparent; color:var(--text-2);
  font-family:inherit; font-size:var(--fs-md); font-weight:600;
  transition:background var(--dur-base) var(--ease), color var(--dur-base) var(--ease);
}
.dtab svg{width:14px; height:14px;}
.dtab[aria-selected="true"]{
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  color:var(--on-accent); font-weight:800; box-shadow:var(--glow-accent);
}
.dtab:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px;}
/* something is open to record today -- the same signal the CTA carries, so a
   participant who lands on another panel still sees there is work waiting */
.dtab-dot{
  width:7px; height:7px; border-radius:50%; background:var(--gold);
  box-shadow:0 0 0 3px rgba(var(--gold-rgb),0.20);
}
.dtab[aria-selected="true"] .dtab-dot{background:var(--on-accent); box-shadow:none;}

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

/* --- "how am I doing": a ring, not four bare numbers ---
   The share is the point, and a number cannot show a share. r=20 gives a
   125.66 circumference, which is what the inline stroke-dasharray is a
   fraction of; the rotation starts the arc at twelve o'clock. */
.mine-card{
  display:flex; align-items:center; gap:var(--space-8);
  padding:var(--space-7); border-radius:var(--radius-md); margin-bottom:var(--space-5);
}
.ring-wrap{position:relative; width:92px; height:92px; flex:none;}
.ring{width:100%; height:100%; transform:rotate(-90deg);}
.ring circle{fill:none; stroke-width:5;}
.ring-track{stroke:var(--track);}
/* one wedge per state -- butt caps so adjoining segments meet with no seam,
   same four colours as .lg-dot / .occ so the ring, the legend and the strip
   never disagree about what a colour means */
.ring-seg{stroke-linecap:butt;}
.ring-seg.completed{stroke:var(--fayrouzeh);}
/* --border-strong, not the dot's --fill-mute: that token is a hair off
   --track in light, so as a stroke the wedge would vanish into the ring it
   is meant to be a share of. The dot gets away with it by drawing an inset
   outline, which a stroke has no way to carry. */
.ring-seg.skipped{stroke:var(--border-strong);}
.ring-seg.missed{stroke:rgba(var(--mohr-rgb),0.62);}
.ring-seg.pending{stroke:rgba(var(--gold-rgb),0.55);}
.ring-center{
  position:absolute; inset:0; display:grid; place-content:center;
  text-align:center; line-height:1.25;
}
.ring-center b{display:block; font-size:var(--fs-2xl); font-weight:800;}
.ring-center span{font-size:var(--fs-xs); color:var(--text-3); font-variant-numeric:tabular-nums;}

.ring-legend{flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--space-4);}
.ring-legend li{
  display:flex; align-items:center; gap:var(--space-4);
  font-size:var(--fs-sm); color:var(--text-2);
}
.ring-legend b{
  margin-inline-start:auto; font-size:var(--fs-lg); font-weight:700;
  color:var(--text-1); font-variant-numeric:tabular-nums;
}
/* one swatch vocabulary shared by the legend, the strip and its key, and
   matching the .badge-mark colours a row further down the panel uses */
.lg-dot{width:9px; height:9px; border-radius:var(--radius-xs); flex:none; display:inline-block;}
.lg-dot.ok{background:var(--fayrouzeh);}
.lg-dot.skip{background:var(--fill-mute); box-shadow:inset 0 0 0 1px var(--border-strong);}
.lg-dot.miss{background:rgba(var(--mohr-rgb),0.62);}
.lg-dot.wait{background:rgba(var(--gold-rgb),0.55);}
/* calendar-only, and both mirror how the cell itself is drawn: "پیش رو" is an
   outline because nothing has been claimed about it yet, and a challenge
   milestone is a dot in the ink rather than a hue of its own */
.lg-dot.soon{background:transparent; box-shadow:inset 0 0 0 1.5px rgba(var(--accent-rgb),0.45);}
.lg-dot.mark{
  background:none; width:9px; height:9px; position:relative;
}
.lg-dot.mark::after{
  content:""; position:absolute; inset:0; margin:auto;
  width:4px; height:4px; border-radius:50%; background:var(--text-2);
}

.mine-empty{
  padding:var(--space-10) var(--space-7); text-align:center; border-radius:var(--radius-md);
  font-size:var(--fs-md); color:var(--text-3); margin:0;
}

/* --- the run at a glance ---
   One square per occurrence, oldest first. A fixed 15-column grid keeps the
   cells square at any history length, and in RTL it fills from the right, so
   the newest occurrence lands nearest the timeline underneath it. */
.occ-strip{display:grid; grid-template-columns:repeat(15, 1fr); gap:var(--space-2);}
.occ{aspect-ratio:1; border-radius:var(--radius-xs); background:var(--track);}
.occ.completed{background:var(--fayrouzeh);}
.occ.skipped{background:var(--fill-mute); box-shadow:inset 0 0 0 1px var(--border-strong);}
.occ.missed{background:rgba(var(--mohr-rgb),0.62);}
.occ.pending{background:rgba(var(--gold-rgb),0.55);}
.occ-key{
  display:flex; flex-wrap:wrap; gap:var(--space-3) var(--space-7); margin-top:var(--space-5);
  font-size:var(--fs-xs); color:var(--text-3);
}
.occ-key span{display:inline-flex; align-items:center; gap:var(--space-3);}

.sub-head-row{display:flex; align-items:center; justify-content:space-between; gap:var(--space-5);}
.sub-head-row .sub-head{margin:var(--space-7) 0 var(--space-4);}
/* .btn-ghost is flex:1 because it normally shares a row of actions; beside a
   heading it has to be exactly as wide as its own label instead */
.btn-xs{
  flex:none; padding:var(--space-3) var(--space-5); font-size:var(--fs-sm); border-radius:var(--radius-pill);
  color:var(--text-2);
}
.btn-xs svg{width:12px; height:12px;}

/* --- the history calendar ---
   The whole run on the days it happened. Structurally the date picker's own
   month grid (.jp-nav / .jp-weekdays / .jp-grid / .jp-cell are reused as-is,
   so the two calendars in the app are laid out by one set of rules); what is
   added here is a cell's *state* colour and the day pane underneath. */
.hist-cal{margin-top:var(--space-5); padding:var(--space-6); border-radius:var(--radius-md);}
.hist-cal .jp-nav{margin-bottom:var(--space-5);}
.hist-cal .btn-nav:disabled{opacity:.25; cursor:not-allowed;}
.hist-cal .btn-nav:disabled:hover{background:transparent;}

/* Same four colours as .lg-dot and .occ -- the ring, the strip and the
   calendar are three views of one tally and may not disagree about what a
   colour means. The number stays legible on top, so each fill carries its
   own ink rather than inheriting the cell's. */
.cal-cell.has-completed{background:var(--fayrouzeh); color:var(--on-accent); font-weight:700;}
.cal-cell.has-skipped{background:var(--border-strong); color:var(--text-1);}
.cal-cell.has-missed{background:rgba(var(--mohr-rgb),0.62); color:var(--on-accent); font-weight:700;}
.cal-cell.has-pending{background:rgba(var(--gold-rgb),0.55); color:var(--on-gold); font-weight:700;}
/* a nobat the cadence still has coming: outlined, never filled. Same call
   .hm-cell.is-future makes on the home grid -- a day that has not happened is
   not a day that was missed, and only a fill may claim an outcome. */
.cal-cell.has-upcoming{
  background:transparent; color:var(--text-2);
  box-shadow:inset 0 0 0 1.5px rgba(var(--accent-rgb),0.45);
}
/* a day this challenge never asked anything of: drawn so the month keeps its
   shape, but recessed -- it is not a day that was missed */
.cal-cell.is-blank{background:transparent; color:var(--text-3); opacity:.55;}

/* A challenge milestone -- start of membership, first/last session, the
   deadline. It is neither a status nor a category, so it may not take a hue
   from either channel: the dot is the cell's own ink, and the day pane names
   it. Sits over .is-blank's dimming so a milestone on an empty day still
   reads. */
/* .jp-cell is unpositioned -- the picker never needed to hang anything off a
   day; the marker dot does */
.cal-cell{position:relative;}
.cal-cell.has-mark{opacity:1;}
.cal-cell.has-mark::after{
  content:""; position:absolute; inset-inline-start:50%;
  transform:translateX(-50%); bottom:3px;
  width:4px; height:4px; border-radius:50%; background:currentColor;
}
.cal-cell.has-mark.is-blank{color:var(--text-2);}
.cal-cell.is-selected{
  outline:2px solid var(--text-1); outline-offset:1px;
}
.hist-cal .cal-key{margin-top:var(--space-5);}

/* the answer to tapping a day, in the month's own card */
.cal-day{
  margin-top:var(--space-6); padding-top:var(--space-6);
  border-top:1px solid var(--border);
}
.cal-day-head{font-size:var(--fs-md); font-weight:700; margin-bottom:var(--space-3);}
.cal-day-empty{margin:0; font-size:var(--fs-sm); color:var(--text-3);}
/* a challenge milestone in the day pane -- quieter than an occurrence row,
   because it is context for the day rather than something to act on */
.cal-mark{
  display:flex; align-items:center; gap:var(--space-4); padding:var(--space-3) 0;
  font-size:var(--fs-sm); font-weight:600; color:var(--text-2);
}
.cal-mark svg{width:13px; height:13px; flex:none; color:var(--text-3);}
.cal-mark + .tl-row{border-top:1px solid var(--border);}
/* the timeline's row, minus the date -- the heading above already said it */
.cal-day .tl-row{padding:var(--space-4) 0;}
.cal-day .tl-row + .tl-row{border-top:1px solid var(--border);}
.cal-note{margin-top:var(--space-2); font-size:var(--fs-sm); color:var(--text-3);}

/* the personal fact list is its own card here; the plan card's copy is
   already inside one */
.fact-list.boxed{padding:var(--space-1) var(--space-7); border-radius:var(--radius-md);}

/* who is behind the collective bar, on the same card as the bar */
.collective-head{
  display:flex; align-items:center; gap:var(--space-5); margin-bottom:var(--space-6);
  font-size:var(--fs-md); color:var(--text-2);
}

/* --- the CTA: one primary action, with leaving beside it ---
   The primary button is whatever the visitor's next move actually is. Only
   when there is nothing left to record does the button itself become
   "leave", and it goes quiet-clay rather than accent so it never reads as
   the happy path. */
.cta-row{display:flex; align-items:stretch; gap:var(--space-5);}
.cta-row .btn-primary{flex:1; margin-top:0;}
.btn-primary.is-leave{
  background:rgba(var(--mohr-rgb),0.16); color:var(--danger-text);
  border:1px solid rgba(var(--mohr-rgb),0.32); box-shadow:none;
}
.cta-side{
  flex:none; width:48px; border-radius:var(--radius-sm); cursor:pointer;
  display:grid; place-items:center;
  background:rgba(var(--mohr-rgb),0.14); border:1px solid rgba(var(--mohr-rgb),0.30);
  color:var(--danger-text);
  transition:transform var(--dur-fast) var(--ease);
}
.cta-side svg{width:17px; height:17px;}
.cta-side[hidden]{display:none;}
.cta-side:focus-visible{outline:2px solid var(--mohr); outline-offset:2px;}

/* ==========================================================================
   Home | the member's own dashboard
   --------------------------------------------------------------------------
   Built out of primitives that already carry meaning elsewhere -- .mine-card
   with its ring, .mine-grid's tiles, .cc-progress's bar -- so a shape a
   member learned on the challenge-detail page means the same thing here.
   Only the two things the home page needs and nowhere else has are new: the
   activity grid, and the ranked focus rows.
   ========================================================================== */

/* the one link out of the dashboard: quiet accent, not a .btn-primary, so it
   never competes with the FAB for "the thing to press" */
.dash-link{
  display:flex; align-items:center; gap:var(--space-4); margin-top:var(--space-5);
  padding:var(--space-6) var(--space-7); border-radius:var(--radius-sm); text-decoration:none;
  background:rgba(var(--accent-rgb),0.10);
  border:1px solid rgba(var(--accent-rgb),0.26);
  color:var(--fayrouzeh); font-size:var(--fs-md); font-weight:700;
}
.dash-link svg{width:15px; height:15px; margin-inline-start:auto;}

/* the enrollment counts, moved off their own stat cards and into the tabs
   that filter by them; .tab is already a pill, so this only has to stay
   subordinate to the label beside it */
.tab-count{
  font-style:normal; font-size:var(--fs-xs); font-weight:700; opacity:.7;
  margin-inline-start:var(--space-3); font-variant-numeric:tabular-nums;
}

/* four figures, so two rows of two rather than .mine-grid's three-up: the
   labels here are phrases («بهترین زنجیرهٔ فعلی»), not the single words the
   challenge-detail tiles carry, and a third column wraps every one of them */
.mine-grid.pairs{grid-template-columns:repeat(2,1fr);}

/* --- the activity grid ---
   One cell per day; seven rows, one column per Saturday-aligned week, so
   every *row* is a single weekday and a habit that always breaks on the same
   day shows as a pale row. The weekday labels are the grid's first column
   rather than a second grid beside it -- that is what keeps them aligned to
   their rows at any cell size. In RTL column 1 lands on the right, so the
   grid reads oldest-right / newest-left like .occ-strip does. */
.hm-card{padding:var(--space-7); border-radius:var(--radius-md);}

.hm-grid{
  display:grid; grid-template-rows:repeat(7, auto);
  grid-auto-flow:column; grid-auto-columns:1fr; gap:var(--space-2);
}
.hm-day{
  display:grid; place-items:center;
  font-size:var(--fs-xs); color:var(--text-3); line-height:1;
}
.hm-cell{aspect-ratio:1; border-radius:var(--radius-xs); background:var(--track);}
/* Four steps of one hue, not four hues: the scale is an amount of the same
   thing. Capped at l4 server-side (HEATMAP_MAX_LEVEL). */
.hm-cell.l1{background:rgba(var(--accent-rgb),0.30);}
.hm-cell.l2{background:rgba(var(--accent-rgb),0.52);}
.hm-cell.l3{background:rgba(var(--accent-rgb),0.76);}
.hm-cell.l4{background:var(--fayrouzeh);}
/* the tail of the current week: drawn, so the grid keeps its rectangle, but
   empty -- a day that has not happened is not a day that was missed */
.hm-cell.is-future{background:transparent; box-shadow:inset 0 0 0 1px var(--track);}
.hm-cell.is-today{outline:1.5px solid var(--fayrouzeh); outline-offset:1px;}

/* month names sit over the week they start, on a grid sharing the cell
   grid's column count so a label never drifts off its week */
.hm-months{
  display:grid; grid-template-columns:repeat(var(--hm-cols), 1fr);
  gap:var(--space-2); margin-bottom:var(--space-3); min-height:13px;
  font-size:var(--fs-xs); color:var(--text-3);
}
.hm-months span{white-space:nowrap;}

/* the grid's own caption: what it adds up to on one side, what its shades
   mean on the other */
.hm-foot{
  display:flex; align-items:center; justify-content:space-between;
  flex-wrap:wrap; gap:var(--space-3) var(--space-6);
  margin-top:var(--space-6); font-size:var(--fs-xs); color:var(--text-3);
}
.hm-scale{display:flex; align-items:center; gap:var(--space-3); font-size:var(--fs-xs);}
/* the swatches are the same .hm-cell rules, but out of the grid they have no
   column to take their width from */
.hm-scale .hm-cell{width:11px; height:11px; aspect-ratio:auto; flex:none;}

/* --- where the effort went ---
   A ranked list, not a stacked bar: split across six categories a
   share-of-total bar is five stubs. Bars scale to the leader so the list
   reads as an order; the count beside each keeps it from being only a
   proportion.
   This is the one surface where the taxonomy *is* the data, so the whole row
   -- tile, word and bar -- runs on the category channel's --cat, set by the
   row's own data-cat (see "Category colour" above). Every reference keeps the
   accent fallback, so a row that arrives without the attribute renders as it
   did before that channel existed rather than colourless. */
.focus-list{display:flex; flex-direction:column; gap:var(--space-6);}
.focus-row{display:flex; align-items:center; gap:var(--space-5);}
.focus-ic{
  width:28px; height:28px; border-radius:var(--radius-sm); flex:none;
  display:grid; place-items:center;
  background:rgba(var(--cat-rgb, var(--accent-rgb)),0.12);
  color:var(--cat, var(--fayrouzeh));
}
.focus-ic svg{width:15px; height:15px;}
.focus-main{flex:1; min-width:0;}
.focus-head{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--space-5);
  margin-bottom:var(--space-3);
}
.focus-head b{
  font-size:var(--fs-md); font-weight:700; color:var(--cat, var(--text-1));
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.focus-head span{font-size:var(--fs-sm); color:var(--text-3); flex:none;}
/* the bar is .cc-progress re-pointed at the row's hue; its accent glow goes
   with the accent fill, or a purple bar sits in a sage halo */
.focus-main .cc-progress{margin-top:0;}
.focus-main .cc-progress > i{
  background:linear-gradient(90deg,
    rgba(var(--cat-rgb, var(--accent-rgb)),0.62), var(--cat, var(--fayrouzeh)));
  box-shadow:none;
}

/* ==========================================================================
   Tour — the guided introduction, driven by tour.js
   --------------------------------------------------------------------------
   Three layers, in this order: a frosted veil over the whole viewport with a
   rounded hole punched in it, a ring drawn around that hole, and the callout
   that explains what is inside it.

   The hole is a `clip-path: path(evenodd, …)` rather than a raised target or
   a giant `box-shadow` spread. It matters which: raising the target with
   z-index dies on the first ancestor that owns a stacking context, and a
   spread shadow can dim the page but cannot *unblur* a window in it, which is
   the effect this tour is built around. Where `path()` is unsupported there
   is no hole to be had, so the @supports fallback drops the blur entirely
   and only dims — a blurred target the tour is pointing at would be worse
   than no frosting at all.
   ========================================================================== */

.tour{position:fixed; inset:0; z-index:var(--z-tour);}

.tour-veil{
  position:fixed; inset:0;
  background:var(--tour-veil);
  /* Deliberately light on both counts: the page behind has to stay legible as
     *context* -- the member is being told where a control lives, and a control
     with no surroundings is a control they cannot find again. The ring below
     does the pointing, so the veil only has to push the rest back a step. */
  backdrop-filter:blur(4.5px) saturate(1.03);
  -webkit-backdrop-filter:blur(4.5px) saturate(1.03);
  /* the veil is the scroll lock: it swallows the gesture rather than the page
     locking `overflow`, which would fight scrollIntoView between steps */
  touch-action:none; overscroll-behavior:contain;
  animation:tourFade .24s var(--ease);
}
@supports not (clip-path: path("M0,0Z")){
  .tour-veil{
    backdrop-filter:none; -webkit-backdrop-filter:none;
    background:var(--scrim);
  }
}

/* The hole's edge, in the primary accent -- «live/primary» is exactly what
   this ring means, and the status and category channels may not be borrowed
   for it. It *breathes*: both the hairline and its halo swell and fade
   together on a slow ease-in-out, which is what makes the eye come back to it
   without the ring ever moving. Not an expanding ping -- a shockwave leaving
   the target reads as "something happened here", while a pulse staying on it
   reads as "look here", and the second is what a tour is for.

   Both keyframes list the same two shadows in the same order, so the browser
   interpolates them instead of swapping; the base state is the midpoint, so a
   reduced-motion viewer (which drops the animation below) still gets a ring
   at full strength rather than whichever end the keyframes happened to start
   from. */
.tour-ring{
  position:fixed; pointer-events:none;
  box-shadow:0 0 0 2px rgba(var(--accent-rgb),0.75), 0 0 21px 3px rgba(var(--accent-rgb),0.30);
  animation:tourBreathe 2.8s ease-in-out infinite;
}
/* On a step that waits for a tap, the ring is the one thing on the overlay
   that takes a pointer -- it lies exactly over the hole, so tapping it is
   tapping the control inside it, and everything else stays swallowed by the
   veil. Colour and geometry are unchanged: it is the same ring, now also the
   button. */
.tour-ring.is-handover{
  pointer-events:auto; cursor:pointer;
  /* Its own beat, faster and wider than the «look here» one: a step waiting
     for a tap is asking for something, and the ring is the button. The scale
     is on the standalone property so it composes with nothing else here and
     stays centred on the hole -- the hole itself never moves, so the target
     underneath is never clipped by the swell. */
  animation:tourTap 1.4s ease-in-out infinite;
}

@keyframes tourTap{
  0%,100%{
    scale:1;
    box-shadow:0 0 0 2px rgba(var(--accent-rgb),0.80), 0 0 16px 1px rgba(var(--accent-rgb),0.26);
  }
  50%{
    scale:1.045;
    box-shadow:0 0 0 3px rgba(var(--accent-rgb),1.00), 0 0 34px 9px rgba(var(--accent-rgb),0.55);
  }
}

/* --- the handover instruction ---
   Rendered only on a step that waits for a tap. It takes the accent and a
   tinted fill because it is the one line on the callout that is an
   instruction rather than a description -- the same accent the ring outside
   is beating in, so the sentence and the ring read as one thing. */
.tour-cta{
  display:flex; align-items:center; gap:var(--space-4);
  margin:var(--space-5) 0 0; padding:var(--space-4) var(--space-5);
  border-radius:var(--radius-sm);
  background:rgba(var(--accent-rgb),0.12);
  border:1px solid rgba(var(--accent-rgb),0.30);
  color:var(--fayrouzeh);
  font-size:var(--fs-md); font-weight:700; line-height:1.7;
}
/* `display:flex` outranks the UA's `[hidden]{display:none}`, so the line has
   to hide itself: without this it shows on every step, which is the opposite
   of what it is for. */
.tour-cta[hidden]{display:none;}
.tour-cta-icon{display:inline-flex; flex:none;}
.tour-cta-icon svg{width:16px; height:16px; animation:tourTapNudge 1.4s ease-in-out infinite;}

/* The finger dips on the same beat the ring swells on, so the two are one
   gesture rather than two loops running past each other. */
@keyframes tourTapNudge{
  0%,100%{transform:translateY(0);}
  50%    {transform:translateY(2px);}
}

@keyframes tourBreathe{
  /* the trough is a floor, not an off state: with the veil this light the
     ring is the only thing marking the target, so it never fades out */
  0%,100%{box-shadow:0 0 0 1.75px rgba(var(--accent-rgb),0.62), 0 0 14px 0 rgba(var(--accent-rgb),0.20);}
  50%    {box-shadow:0 0 0 2.5px rgba(var(--accent-rgb),1.00), 0 0 30px 6px rgba(var(--accent-rgb),0.45);}
}

/* --- the callout ---
   Positioned by tour.js in viewport coordinates, so it takes `left`/`top`
   rather than a logical inset: the maths already accounts for RTL by working
   from the target's centre outwards. */
.tour-pop{
  position:fixed; left:0; top:0;
  width:min(330px, calc(100vw - 24px));
  --tour-arrow:9px;    /* half the rotated square's side; ARROW in tour.js clamps to it */
  padding:var(--space-7) var(--space-8) var(--space-8);
  border-radius:var(--radius-md);
  /* Opaque, like .sheet-panel and for the same reason twice over: the callout
     sits on a surface that is already frosted, so a second translucent pane
     only muddies it -- and the arrow below overlaps this background by half
     its width, which shows as a seam the moment either one is see-through. */
  /* The frame is what makes the callout findable: this is the only pane on
     the screen that *moves* between steps, so a neutral hairline leaves the
     member hunting for where the sentence went.

     It is warm (--mohr) while the ring outside stays the cool accent, and the
     split is deliberate: two panes in one colour read as one object and the
     eye stops separating them, whereas a warm frame around a cool ring reads
     as *the pair* -- the ring points, the framed pane speaks. Warm also puts
     nothing at risk here, since the tour paints no status and no category.

     The halo is listed before --shadow-lift so the panel still casts its own
     depth over the glow rather than under it. */
  background:var(--bg-1); border:2px solid rgba(var(--mohr-rgb),0.85);
  box-shadow:0 0 0 5px rgba(var(--mohr-rgb),0.18),
             0 0 34px 4px rgba(var(--mohr-rgb),0.38),
             var(--shadow-lift);
  /* Hidden until the first measurement lands, or the callout is visibly
     dragged from the corner to its place on every step. */
  opacity:0; transition:opacity var(--dur-base) var(--ease);
}
.tour.is-ready .tour-pop{opacity:1; animation:tourPopIn .22s var(--ease);}

.tour-arrow{
  position:absolute; width:calc(var(--tour-arrow) * 2); height:calc(var(--tour-arrow) * 2);
  margin-left:calc(var(--tour-arrow) * -1);
  /* Same weight and colour as the panel's frame, or the tip reads as a
     separate scrap pinned to an accented pane. */
  background:var(--bg-1); border:2px solid rgba(var(--mohr-rgb),0.85);
  transform:rotate(45deg); border-radius:var(--radius-xs);
}
/* Only the two edges facing away from the callout are drawn; the other two
   would print a seam across the panel they are supposed to be part of. */
/* Physical sides, not logical: the square is rotated in physical space, so
   which two of its edges end up facing the target has nothing to do with the
   writing direction. */
.tour-pop[data-arrow="top"] .tour-arrow{
  top:calc(var(--tour-arrow) * -1 + 2px);
  border-right:none; border-bottom:none;
}
.tour-pop[data-arrow="bottom"] .tour-arrow{
  bottom:calc(var(--tour-arrow) * -1 + 2px);
  border-left:none; border-top:none;
}
.tour-pop[data-arrow="none"] .tour-arrow{display:none;}

.tour-pop-head{display:flex; align-items:center; justify-content:space-between; gap:var(--space-5);}
.tour-count{
  font-size:var(--fs-xs); font-weight:700; color:var(--fayrouzeh);
  letter-spacing:.02em; font-variant-numeric:tabular-nums;
}
.tour-x{width:28px; height:28px;}
.tour-x svg{width:15px; height:15px;}

.tour-pop h3{font-size:var(--fs-lg); font-weight:800; margin:var(--space-3) 0 var(--space-3);}
.tour-pop p{font-size:var(--fs-md); line-height:1.85; color:var(--text-2); margin:0;}

.tour-foot{
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-6);
  margin-top:var(--space-7);
}
/* Progress as dots, not a bar: seven steps is a countable number, and a
   number the reader can count is a promise the tour is short. */
.tour-dots{display:flex; align-items:center; gap:var(--space-3);}
.tour-dots i{
  width:5px; height:5px; border-radius:50%; background:var(--track);
  transition:width var(--dur-base) var(--ease), background-color var(--dur-base) var(--ease);
}
.tour-dots i.on{width:16px; border-radius:var(--radius-xs); background:var(--fayrouzeh);}

.tour-btns{display:flex; gap:var(--space-4);}
.tour-btn{
  padding:var(--space-4) var(--space-8); border-radius:var(--radius-sm); cursor:pointer;
  font-size:var(--fs-md); font-weight:700;
  background:var(--fill-2); border:1px solid var(--border);
  color:var(--text-2);
}
.tour-btn[disabled]{opacity:.4; cursor:default;}
.tour-btn:focus-visible{outline:2px solid var(--fayrouzeh); outline-offset:2px;}
/* the shell is focused on every step so a screen reader reads the new copy;
   it is not a control, so it must not look like one */
.tour-pop:focus{outline:none;}
.tour-btn.primary{
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  border-color:transparent; color:var(--on-accent);
  box-shadow:var(--glow-accent);
}

@keyframes tourFade{from{opacity:0;} to{opacity:1;}}
@keyframes tourPopIn{from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:none;}}

/* Same split as the press feedback above: the spotlight is information and
   stays, the breathing and the sliding are decoration and go. */
@media (prefers-reduced-motion:reduce){
  .tour-veil, .tour-pop{animation:none;}
  /* The handover ring loses its beat like every other ring, but keeps the
     stronger resting shadow -- «this one wants a tap» is information, and the
     sentence in .tour-cta carries it either way. */
  .tour-ring, .tour-ring.is-handover, .tour-cta-icon svg{animation:none;}
  .tour-ring.is-handover{box-shadow:0 0 0 3px rgba(var(--accent-rgb),1.00), 0 0 30px 6px rgba(var(--accent-rgb),0.45);}
  .tour-dots i{transition:none;}
}


/* ==========================================================================
   Leaderboard
   ==========================================================================
   One challenge's standings. Built entirely out of tokens that already
   exist — no new colour is introduced here, and none is hardcoded.

   Colour on this screen means one thing: *rank*. It borrows `--gold`, which
   already means "achievement" everywhere else in the app (the streak tiles,
   the record tile), and it borrows neither the `--st-*` status hues nor the
   six `--cat-*` category ones — see the two-channel rule in CLAUDE.md. The
   only category colour on the page is on the header card, where it names the
   challenge rather than a person's position.

   The podium is three rows, not a separate block: a raised trio above a list
   is a second layout to keep in sync and re-reads the same three people
   twice. `[data-podium]` just tints the rank badge of the top three, so a
   tie that puts four people at rank 1 simply tints four badges instead of
   breaking a fixed three-slot frame. */

.lb-page{padding:var(--space-2) 0 var(--space-12);}

/* --- whose board this is --- */
.lb-head{
  display:flex; align-items:center; gap:var(--space-6);
  margin:0 var(--space-10) var(--space-7); padding:var(--space-7) var(--space-7);
  text-decoration:none; color:inherit;
}
.lb-head-icon{
  width:38px; height:38px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:color-mix(in srgb, var(--cat, var(--fayrouzeh)) 16%, transparent);
  color:var(--cat, var(--fayrouzeh));
}
.lb-head-icon svg{width:19px; height:19px;}
.lb-head-text{min-width:0; flex:1;}
.lb-head-text b{
  display:block; font-size:var(--fs-lg); font-weight:700; line-height:1.5;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.lb-head-text span{display:block; font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}

/* --- «رتبه تو» --- */
/* The one figure a member opened this page for, above the list rather than
   only inside it. The rank is the largest thing on the screen on purpose:
   everything else here is context for it. */
.lb-mine{
  margin:0 var(--space-10) var(--space-7); padding:var(--space-7);
  display:grid; grid-template-columns:auto 1fr; gap:var(--space-7); align-items:center;
}
.lb-mine-rank{
  text-align:center; padding:var(--space-5) var(--space-7); border-radius:var(--radius-md);
  background:rgba(var(--gold-rgb),0.12);
  border:1px solid rgba(var(--gold-rgb),0.28);
}
.lb-mine-rank b{
  display:block; font-size:var(--fs-4xl); font-weight:800; line-height:1;
  color:var(--gold);
}
.lb-mine-rank span{display:block; font-size:var(--fs-xs); color:var(--text-3); margin-top:var(--space-3);}
.lb-mine-figs{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-4);}
.lb-mine-figs div{text-align:center;}
.lb-mine-figs b{
  display:block; font-size:var(--fs-xl); font-weight:800; line-height:1.2;
  white-space:nowrap;
}
.lb-mine-figs span{display:block; font-size:var(--fs-xs); color:var(--text-3); margin-top:var(--space-2);}
.lb-mine-figs .gold b{
  color:var(--gold); display:flex; align-items:center; justify-content:center; gap:var(--space-2);
}
.lb-mine-figs .gold b svg{width:13px; height:13px;}
/* Spans both columns: it acts on the list below, not on the card it sits in. */
.lb-mine .btn{grid-column:1 / -1; justify-self:start;}

/* --- the standings --- */
.lb-list{display:flex; flex-direction:column; gap:var(--space-4); padding:0 var(--space-10);}
.lb-row{
  display:flex; align-items:center; gap:var(--space-6);
  padding:var(--space-5) var(--space-6); border-radius:var(--radius-md);
  --press-scale:.985;
}
.lb-rank{
  min-width:30px; text-align:center; flex-shrink:0;
  font-size:var(--fs-lg); font-weight:800; color:var(--text-3);
  font-variant-numeric:tabular-nums;
}
/* Gold for first, and a quieter step down for second and third — the same
   hue at lower strength rather than three different colours, so the podium
   reads as one idea instead of three unrelated marks. */
.lb-row[data-podium] .lb-rank{
  color:var(--gold);
  border-radius:var(--radius-sm); padding:var(--space-2) 0;
}
.lb-row[data-podium="1"] .lb-rank{background:rgba(var(--gold-rgb),0.20);}
.lb-row[data-podium="2"] .lb-rank{background:rgba(var(--gold-rgb),0.12);}
.lb-row[data-podium="3"] .lb-rank{background:rgba(var(--gold-rgb),0.07);}

/* Opaque neutral behind every avatar: several catalogue styles ship a
   transparent ground, so whatever sits behind the <img> becomes part of the
   picture (CLAUDE.md, Avatars). */

.lb-who{min-width:0; flex:1;}
.lb-who b{
  display:flex; align-items:center; gap:var(--space-3);
  font-size:var(--fs-lg); font-weight:700; line-height:1.5;
  overflow:hidden; white-space:nowrap;
}
.lb-you{
  flex-shrink:0; font-size:var(--fs-xs); font-weight:700; padding:1px var(--space-4);
  border-radius:var(--radius-pill); background:var(--fayrouzeh); color:var(--on-accent);
}
.lb-other{display:block; font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}

.lb-score{text-align:center; flex-shrink:0; min-width:46px;}
.lb-score b{display:block; font-size:var(--fs-xl); font-weight:800; line-height:1.15;}
.lb-score span{display:block; font-size:var(--fs-xs); color:var(--text-3); margin-top:var(--space-1);}

/* Your own row, marked by its edge rather than by a fill: the list is
   scanned top to bottom and a filled row would out-shout the podium it sits
   below, which is the one thing this screen is actually about. */
.lb-row.is-you{
  border-color:rgba(var(--accent-rgb),0.45);
  box-shadow:var(--shadow-soft), inset 2px 0 0 var(--fayrouzeh);
}
/* «جای من» replays this on every tap — the row may already be on screen, in
   which case scrolling to it is invisible and the flash is the whole answer. */
.lb-row.is-found{animation:lbFound 1.1s ease-out;}
@keyframes lbFound{
  0%{background:rgba(var(--accent-rgb),0.18);}
  100%{background:var(--glass);}
}

/* --- the way in, on challenge-detail's «من» panel --- */
.lb-entry{
  display:flex; align-items:center; gap:var(--space-6);
  margin-bottom:var(--space-6); padding:var(--space-6) var(--space-7);
  text-decoration:none; color:inherit;
  --press-scale:.985;
}
.lb-entry-icon{
  width:36px; height:36px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:rgba(var(--gold-rgb),0.14); color:var(--gold);
}
.lb-entry-icon svg{width:18px; height:18px;}
.lb-entry-text{min-width:0; flex:1;}
.lb-entry-text b{display:block; font-size:var(--fs-lg); font-weight:700; line-height:1.5;}
.lb-entry-text span{display:block; font-size:var(--fs-sm); color:var(--text-3); margin-top:var(--space-1);}

/* --- «نمایش تو»: با نام / ناشناس (challenge-detail, panel «من») ---
   A two-state answer, so the app's own `.seg` rather than a switch: both
   states are named on screen and the active one is the highlighted one, in
   one compact row instead of a settings row with two lines of hint. The
   explanation lives in the row's `title` and in the toast the change
   raises. Borrows no new colour -- the accent is the same "this is the
   selected option" it means on every other `.seg` in the app. */
.ident-switch{
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-5);
  margin-bottom:var(--space-6); padding:var(--space-4) var(--space-6);
}
.ident-label{font-size:var(--fs-md); font-weight:600; color:var(--text-2); flex-shrink:0;}
.ident-switch .seg{flex:0 0 auto; background:var(--fill-1); border-color:transparent;}
.ident-switch .btn-seg{flex:0 0 auto; padding:var(--space-3) var(--space-6); font-size:var(--fs-sm); --press-scale:.94;}

@media (prefers-reduced-motion:reduce){
  .lb-row.is-found{animation:none;}
}

/* The leaderboard's ordering control is anchors rather than buttons (see
   challenge/leaderboard.html: switching the sort re-ranks the summary card
   too, so it is a navigation and not client state). `.btn-seg` was written
   for <button>, which carries neither of these. */
a.btn-seg{text-decoration:none;}
a.btn-seg.active{color:var(--on-accent);}

/* ==========================================================================
   Groups — app/templates/group/*
   ==========================================================================
   A group borrows the app's existing primitives rather than growing a look
   of its own: the settings screen's `.setting-group`/`.setting-row` for
   every list somebody administers, challenge-detail's `.detail-hero` and
   `.detail-tabs` for the group page, and `.pill` for every label.

   **Colour means nothing new here.** Status owns the `--st-*` hues and
   category owns the six `--cat-*` ones (see "Colour carries two channels"),
   and a group is neither -- so a group's identity is carried by its emblem,
   which is a picture from the one avatar catalogue, and by nothing else.
   The only colour this section spends is the accent, on the two things that
   are genuinely states: an invite link that still works, and the ink on a
   mandatory challenge's own tag. */

/* --- the group card (group/_group_cards.html) --- */
.group-list{display:flex; flex-direction:column; gap:var(--space-5); padding:0 var(--space-10) var(--space-6);}
.group-card{
  display:flex; align-items:center; gap:var(--space-6); padding:var(--space-6) var(--space-7);
  border-radius:var(--radius-md); text-decoration:none; color:inherit;
}
/* An opaque ground under the picture, for the reason `.avatar` has
   one: several catalogue styles ship a transparent SVG, so whatever sits
   behind the <img> becomes part of the emblem. A translucent --fill-* token
   would let the page through. */
.gc-emblem, .gh-emblem{
  border-radius:var(--radius-md); background:var(--bg-1); flex-shrink:0;
  display:grid; place-items:center; overflow:hidden;
}
.gc-emblem{width:44px; height:44px;}
.gc-emblem img{width:44px; height:44px; display:block;}
.gc-body{min-width:0; flex:1;}
.gc-body h3{margin:0; font-size:var(--fs-lg); font-weight:700; line-height:1.5;}
.gc-body p{
  margin:var(--space-2) 0 0; display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap;
  font-size:var(--fs-sm); color:var(--text-3);
}
.gc-body p svg{width:13px; height:13px;}
.gc-chev{width:16px; height:16px; color:var(--text-3); flex-shrink:0;}
.empty .empty-hint{margin-top:var(--space-5); font-size:var(--fs-md); color:var(--text-3);}

/* --- the group page's hero --- */
.group-hero{text-align:center;}
.gh-emblem{width:72px; height:72px; margin:0 auto var(--space-5);}
.gh-emblem img{width:72px; height:72px; display:block;}
/* «زیرگروهی از …» -- the way back up out of a subgroup. Neutral ink and the
   hero's own small type: it is a fact about where you are, not a state, so it
   takes no colour channel (status keeps `--st-*`, category `--cat-*`). */
.gh-parent{display:flex; align-items:center; justify-content:center; gap:var(--space-3);
  margin:var(--space-1) 0 var(--space-4); font-size:var(--fs-md); color:var(--text-3);}
.gh-parent [data-icon]{width:14px; height:14px; flex-shrink:0;}
.gh-parent a{color:var(--text-2); text-decoration:none;}

/* --- the group chip on a challenge card / the detail hero ---
   A tag, not a colour: see the section note above. It reads as tappable
   because it is, and it keeps `.pill`'s own metrics so a row of chips
   stays one row of chips. */
.cc-group{margin:var(--space-2) 0 0 !important; display:flex; gap:var(--space-3); flex-wrap:wrap;}
.pill-group{
  cursor:pointer; text-decoration:none; color:var(--text-2);
  background:var(--fill-1); border-color:var(--border);
}
.pill-group:hover{background:var(--fill-2);}
.pill-tag.warn{color:var(--gold); border-color:rgba(var(--gold-rgb),0.4);
  background:rgba(var(--gold-rgb),0.12);}

/* --- roster rows (group/_member_rows.html, group/manage.html) --- */
.mr-you{font-size:var(--fs-sm); color:var(--text-3); font-weight:500;}
.mr-action{flex-shrink:0;}
.btn-sm{flex:0 0 auto; padding:var(--space-3) var(--space-6); font-size:var(--fs-sm);}

/* The decided-request pill. Neutral in both states on purpose: colour in
   this app means status (--st-*) or category (--cat-*), a request is
   neither, and painting «ردشده» in the danger red would read as an error
   when it is only an answer somebody gave. The word carries it, which is
   the rule everywhere else too. */
.pill-request{flex-shrink:0; font-size:var(--fs-xs);}

/* --- invite links ---
   The pill is the link's *derived* state (`invite_state`), so it always
   says what the link actually does. Only "active" earns the accent: the
   other three are all versions of "this does nothing now", and giving each
   its own hue would invent three distinctions nobody acts on differently. */
.pill-invite{flex-shrink:0; font-size:var(--fs-xs);}
.pill-invite.is-active{
  background:rgba(var(--accent-rgb),0.14); color:var(--fayrouzeh);
  border-color:rgba(var(--accent-rgb),0.34);
}
/* The leading tile on a link row. `.setting-row` rather than `.invite-row`,
   because the public link's own row wears the same mark and is deliberately
   not one of them (see manage.html on why it is its own section). */
.setting-row .mi-icon{
  width:36px; height:36px; border-radius:var(--radius-sm); flex-shrink:0;
  display:grid; place-items:center;
  background:rgba(var(--accent-rgb),0.1); color:var(--fayrouzeh);
}
.setting-row .mi-icon svg{width:17px; height:17px;}
.setting-row.danger-row .sr-label{color:var(--danger-text);}

/* --- managing a group (group/manage.html) ---
   A settings screen, and the redesign that made it one is a rule about
   *marks*: a row ends in the glyph for what tapping it does. A chevron means
   a page opens, a pencil means an editor opens, and an icon button means the
   thing happens right here. The screen it replaced ended every row in a
   chevron and explained the difference in a paragraph underneath, which is
   how «مشخصات گروه — نام، توضیح، نوع و نشان» came to read as a place rather
   than as four values somebody can change.

   Nothing here is a status or a category, so the two colour channels stay
   out (see the section note above). The one accent this screen spends is on
   the button that *starts* something -- `+`, `✓`, copy -- so scanning it
   finds the actions without reading a word. The undoing half (`✕`, `↻`) is
   deliberately neutral: an equally loud pair would make the reader stop and
   read, which is the opposite of what a two-mark answer is for. */

/* A heading that carries a control. `.setting-group > h2`'s own metrics, so
   a section with an action and one without look like the same section. */
.sg-head{display:flex; align-items:center; gap:var(--space-4); margin:0 var(--space-1); min-height:30px;}
.sg-head h2{
  font-size:var(--fs-md); font-weight:700; color:var(--text-3); margin:0;
  letter-spacing:.2px;
}
.sg-head .icon-btn{margin-inline-start:auto;}
/* How many answers are owed. A number rather than a dot, for the reason the
   bell's badge is one: "someone is waiting" and "eleven people are waiting"
   are different messages. It hides itself at zero rather than reading «۰». */
.sg-count{
  min-width:20px; height:20px; padding:0 var(--space-3); border-radius:var(--radius-pill);
  display:grid; place-items:center;
  font-size:var(--fs-sm); font-weight:800; line-height:1;
  background:rgba(var(--accent-rgb),.14); color:var(--fayrouzeh);
}
.sg-count[hidden]{display:none;}
/* An empty section, said once and quietly. The full-page `.empty` block is
   48px of padding and a 44px glyph, which on a screen where "nothing is
   waiting" is the normal state shouts about the good news. */
.sg-note{
  margin:0; padding:var(--space-6) var(--space-2); text-align:center;
  font-size:var(--fs-md); color:var(--text-3);
}
.sg-note[hidden]{display:none;}

/* A row whose text is one line -- a label and its value -- does not need the
   14px a two-line row does. Every row on this screen that dropped its
   explanatory sentence took this instead. */
.setting-row.compact{padding:var(--space-6) var(--space-7);}
.setting-row.compact .sr-head{gap:var(--space-5);}

/* The trailing controls. They sit at the end of `.sr-head` where the chevron
   would be, so a list mixing "opens a page" rows with "acts here" rows still
   scans down one column. */
.sr-actions{display:flex; align-items:center; gap:var(--space-3); flex-shrink:0;}
/* The row-sized icon button: the 40px topbar circle is a target for a thumb
   crossing the whole screen, while these sit inside a 44px row and two of
   them share its end. 30px keeps the row's height and still clears the 24px
   the pointer needs. */
.icon-btn.xs{width:30px; height:30px;}
.icon-btn.xs svg{width:15px; height:15px;}
/* The affirmative half of every pair on this screen -- add, approve, copy.
   The accent is doing what it does everywhere else: marking the one thing
   the reader most likely came to press. */
.icon-btn.accent{
  background:rgba(var(--accent-rgb),.14); border-color:rgba(var(--accent-rgb),.34);
  color:var(--fayrouzeh);
}
.icon-btn.accent:hover{background:rgba(var(--accent-rgb),.22); color:var(--fayrouzeh);}

/* «این ردیف ویرایش می‌شود». A pencil where a chevron would be: the row shows
   its current value, and the mark says the value is the thing you change --
   which a chevron, meaning "a page lives behind this", never said. Sized and
   coloured exactly like `.sr-chev` so the two read as one column. */
.sr-edit{width:16px; height:16px; color:var(--text-3); flex-shrink:0;}
.setting-row.danger-row .sr-edit{color:var(--danger-text);}
/* The emblem as a row's own value, the same "opaque ground under a picture"
   every avatar surface needs (several catalogue styles ship transparent). */
.sr-emblem{
  width:30px; height:30px; border-radius:var(--radius-sm); flex-shrink:0;
  background:var(--bg-1); object-fit:cover;
}
/* The exit rows live inside a tab panel rather than on a settings screen,
   so they bring their own top gap instead of the page's. */
.group-exit{margin-top:var(--space-7);}

/* --- the invite landing (group/invite.html) --- */
.invite-card{
  margin:var(--space-12) var(--space-10); padding:28px var(--space-11) var(--space-11); border-radius:var(--radius-lg);
  text-align:center;
}
.invite-eyebrow{margin:0 0 var(--space-2); font-size:var(--fs-md); color:var(--text-3);}
.invite-card h1{margin:0 0 var(--space-6); font-size:var(--fs-2xl); font-weight:800;}
.invite-card .detail-tags{justify-content:center;}
.invite-desc{margin:var(--space-7) 0 0; font-size:var(--fs-md); line-height:2; color:var(--text-2);}
.invite-note{margin:var(--space-8) 0 var(--space-9); font-size:var(--fs-md); line-height:2; color:var(--text-3);}
.btn-block{flex:none; width:100%; padding:var(--space-6); font-size:var(--fs-lg);}
/* The flat accent button. Not .btn-primary: that one is the page's single
   full-width call to action and carries the gradient and the lifted glow,
   while this is the affirmative half of a pair inside a sheet or a card. */
.btn-accent{
  background:var(--fayrouzeh); color:var(--on-accent); border-color:transparent;
  box-shadow:var(--glow-accent);
}
/* The background is restated here on purpose: `.btn-ghost:hover` is written
   above with the same specificity and would otherwise repaint this button
   with --glass-strong -- a near-white sheet under --on-accent's white ink,
   which is a button whose label disappears the moment a pointer touches it.
   Only a device with a real pointer ever saw it, which is why it shipped. */
.btn-accent:hover{background:var(--fayrouzeh); filter:brightness(1.05);}

/* --- the create wizard's group extras --- */
.wizard-group{display:flex; align-items:center; gap:var(--space-5);}
.wg-emblem{border-radius:var(--radius-sm); background:var(--bg-1); flex-shrink:0;}
.member-pick{
  display:flex; flex-direction:column; gap:var(--space-3); max-height:240px; overflow-y:auto;
  padding:var(--space-2); border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--fill-1);
}
.mp-item{
  display:flex; align-items:center; gap:var(--space-5); padding:var(--space-4) var(--space-5);
  border-radius:var(--radius-sm); cursor:pointer; font-size:var(--fs-md);
}
.mp-item:hover{background:var(--fill-2);}
.mp-item img{width:28px; height:28px; border-radius:var(--radius-sm); background:var(--bg-1);}
.mp-item input{accent-color:var(--fayrouzeh); width:16px; height:16px; flex-shrink:0;}

/* A mandatory group challenge has no "leave" -- the CTA says why instead of
   offering a control that answers 403. Muted rather than accented: it is a
   statement, not an action. */
.btn-primary.is-locked{
  background:var(--fill-1); color:var(--text-3); box-shadow:none;
  border:1px solid var(--border); cursor:default;
}

/* One press animation for everything tappable, tuned per component (see
   "Press feedback"): a group card is the size of a challenge card, so it
   sinks by the same amount. */
.group-card{--press-scale:.985;}

/* An empty state *inside* a group of settings rows, not a whole page: the
   sections above and below it stay in view, so it is padded like the admin
   panel's own (`.admin-members .empty`) rather than like a page's. */
.settings-page .setting-group > .empty{padding:28px var(--space-12);}
.settings-page .setting-group > .empty svg{width:34px; height:34px; margin-bottom:var(--space-4);}

/* ==========================================================================
   راهنمای درجا — the in-place explainer (app/explainers.py, initExplainers())
   --------------------------------------------------------------------------
   A screen full of derived figures needs explaining, and a screen explained
   line by line is a screen nobody reads. So the explanation costs the resting
   layout one 17px dot: quiet enough to scan past, close enough to the thing
   it describes that it never has to be searched for.

   The dot is deliberately *not* accented at rest -- an accent means "act on
   this", and help is never the primary action on any screen here. It takes
   the accent only while its popover is open, which is also the only moment
   the colour means something: this is the one that is talking.
   ========================================================================== */
.explain-dot{
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  width:17px; height:17px; padding:0; margin-inline-start:var(--space-3);
  vertical-align:middle; cursor:pointer;
  border-radius:var(--radius-pill); border:1px solid var(--border-strong);
  background:var(--fill-1); color:var(--text-3);
  font-family:inherit; font-size:var(--fs-xs); font-weight:700; line-height:1;
  transition:color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease);
  /* it is small, so the squeeze has to be deep enough to be felt at all */
  --press-scale:.84;
}
.explain-dot:hover{color:var(--fayrouzeh); border-color:rgba(var(--accent-rgb),0.45);}
.explain-dot[aria-expanded="true"]{
  background:rgba(var(--accent-rgb),0.14);
  border-color:rgba(var(--accent-rgb),0.5);
  color:var(--fayrouzeh);
}
/* The dot rides beside a heading or a label, so a host that *has* one becomes
   a row -- scoped with :has() rather than by listing the hosts, so adding a
   dot somewhere new needs no rule of its own. Where :has() is unsupported the
   heading stays a block and the dot simply sits inline after its text, which
   is the same reading order and one degraded alignment. */
:is(h1,h2,h3,h4,.field-label,.sr-title,.stat-label,.fact-label,.field label):has(> .explain-dot){
  display:inline-flex; align-items:center;
}
/* A tile's caption is 10px and centred, so the dot has to shrink with it --
   at full size it would out-weigh the label it belongs to. */
.mine-tile span:has(> .explain-dot){
  display:flex; align-items:center; justify-content:center;
}
.mine-tile .explain-dot{width:14px; height:14px; font-size:var(--fs-xs); margin-inline-start:var(--space-2);}

/* --- the popover ---------------------------------------------------------
   Positioned by JS in viewport coordinates (fixed), because the dot can sit
   inside a scrolling card, a sticky header or a sheet, and an absolutely
   positioned pane would be clipped by the first ancestor with overflow.
   Opaque like .sheet-panel, and for the second reason too: the arrow overlaps
   this background by half its width, and a translucent pane shows the seam. */
.explain-pop{
  position:fixed; z-index:var(--z-tour); visibility:hidden;
  width:max-content; max-width:min(300px, calc(100vw - 24px));
  padding:var(--space-7) var(--space-8); border-radius:var(--radius-md);
  background:var(--bg-1); border:1px solid var(--border-strong);
  box-shadow:var(--shadow-lift);
  text-align:start; white-space:normal;
  /* The open state is carried by these two properties, not by a keyframe:
     the pane is measured and placed the frame it becomes visible, and an
     animation that has not started yet (a backgrounded tab, a paused
     compositor) would leave it sitting at its 0% opacity. A transition has no
     such state -- the resting value is the one in `.is-open`. */
  opacity:0; transform:translateY(-4px);
  transition:opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.explain-pop.is-open{visibility:visible; opacity:1; transform:none;}
.explain-pop h4{
  margin:0 0 var(--space-3); font-size:var(--fs-md); font-weight:800; color:var(--text-1);
}
.explain-pop p{margin:0; font-size:var(--fs-md); line-height:1.9; color:var(--text-2);}
/* Named states read as a key, not as prose: one line each, the name in ink
   and the meaning behind it stepped back. */
.explain-pop ul{
  list-style:none; margin:var(--space-5) 0 0; padding:var(--space-5) 0 0;
  border-top:1px solid var(--border-strong);
  display:flex; flex-direction:column; gap:var(--space-3);
}
.explain-pop li{font-size:var(--fs-md); line-height:1.75; color:var(--text-3);}
.explain-pop li b{color:var(--text-1); font-weight:700;}
/* A rotated square carrying two of the pane's own borders; JS places it and
   sets [data-side] so the right two edges are the ones that show. */
.explain-arrow{
  position:absolute; width:11px; height:11px; transform:rotate(45deg);
  background:var(--bg-1); border:1px solid var(--border-strong);
}
/* Physical sides, not logical ones: the square is rotated 45deg, so which two
   edges form the visible tip is a fact about the rotation and does not flip
   with the writing direction the way border-inline-* would. */
.explain-pop[data-side="bottom"] .explain-arrow{
  top:-6px; border-right:none; border-bottom:none;
}
.explain-pop[data-side="top"] .explain-arrow{
  bottom:-6px; border-left:none; border-top:none;
}
/* Reduced motion keeps the fade and drops the travel -- the same trade the
   press feedback makes. */
@media (prefers-reduced-motion:reduce){
  .explain-pop{transform:none; transition:opacity var(--dur-fast) var(--ease);}
}

/* ==========================================================================
   لایک
   --------------------------------------------------------------------------
   No chrome: an icon and a number, not a pill. On a card this sits under the
   status pill, and a second bordered capsule there competes with the pill for
   the same glance -- the pill is a *state* the reader is scanning for, the
   heart is an action they either want or don't.

   It is coloured at rest, not only once liked: `--mohr` already means «warm,
   personal» in this UI, and a heart that only becomes visible after the tap
   reads as disabled. Liked is then the *filled* heart plus a slightly deeper
   ink -- shape carries the state, so it survives a greyscale screenshot and
   does not depend on the reader remembering which red meant which.
   ========================================================================== */
.btn-social {
  gap:var(--space-3);
  padding:var(--space-1);
  color: var(--text-3);
  font-weight: 400;
  font-size:var(--fs-sm);
  line-height: 1;
  /* The press squeeze is global (`initPressFeedback`); only its depth is
     tuned here, which is why there is no `:active` rule. */
  --press-scale: .88;
}
.btn-social [data-icon] { width: 13px; height: 13px; display: inline-flex; }
.btn-social [data-icon] svg { width: 100%; height: 100%; stroke-width: 1.6; }
.btn-social b { font-weight: 600; font-variant-numeric: tabular-nums; }
/* the heart is the one of the two that carries a colour and a state */
.btn-like { color: var(--mohr); }
.btn-like.is-liked { color: var(--mohr-dim); }

/* The heart alone grows, not the row: a control that resizes shifts the card
   body beside it. */
.btn-like.is-liked [data-icon] { animation: likePop .28s ease-out; }
@keyframes likePop {
  0% { scale: 1; }
  45% { scale: 1.3; }
  100% { scale: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .btn-like.is-liked [data-icon] { animation: none; }
}

/* The hero's copy: same control, sized for a page rather than a card. */
.btn-social.lg { font-size:var(--fs-lg); gap:var(--space-4); }
.btn-social.lg [data-icon] { width: 17px; height: 17px; }

/* --- the conversation's control -----------------------------------------
   The heart's twin: same geometry, same press depth, same «icon and a number
   with no chrome». What it does *not* share is the colour -- `--mohr` means
   «warm, personal» and belongs to the heart alone, so the bubble takes the
   neutral ink every other quiet action on a card takes. Nothing about it
   carries state: a conversation is not something you are in or out of. */
/* The hero's row: the two things a viewer does *to* a challenge, side by
   side. A row rather than two margins, so neither control has to know the
   other exists. */
.hero-social {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap:var(--space-4);
  margin-top:var(--space-7); padding-top:var(--space-6);
}
/* The line that separates «how is it going» from «what can I do about it».
   A gradient, not a border, for the reason `.detail-stats`' dividers are one:
   this sits in the pane's faded tail, and a hairline at full strength there
   draws the edge the card is busy dissolving. */
.hero-social::before {
  content: ""; position: absolute; top: 0; inset-inline: 12%; height: 1px;
  background: linear-gradient(to left, transparent, var(--border), transparent);
}
/* Sized as a tap target rather than a run of text: on a page (unlike on a
   card) these are the only two things to hit, and a bare 17px glyph is under
   every touch-target minimum there is. The pill stays invisible at rest --
   «icon and a number, no chrome» is the rule both controls are drawn by --
   and only lights up under the finger. */
.hero-social .btn-social.lg {
  padding:var(--space-4) var(--space-8); border-radius:var(--radius-pill);
  min-height: 40px;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.hero-social .btn-social.lg.is-pressed { background: var(--fill-1); }

/* --- on an explore card -------------------------------------------------
   Status and like stack in one column at the card's outer edge (left, in
   RTL): the pill answers «should I act on this», the heart is how you act,
   and keeping them in one column leaves the card's text lines to the text.
   `.explore-head` therefore aligns to the top rather than centring. --- */
/* The pill takes the first line and the two actions share the second, rather
   than all three stacking: a three-row column made the row twice as tall as
   its own text and left a hole down the middle of the card. */
.ec-side { display: flex; flex-direction: column; align-items: flex-end; gap:var(--space-3); }
.ec-acts { display: flex; align-items: center; gap:var(--space-6); }

/* ==========================================================================
   گفتگو — the comment page and its composer
   --------------------------------------------------------------------------
   Three parts, and only the third is new to this app: a list of threads built
   out of the same neutral card the rest of the app uses, a composer floating
   above the nav exactly like `.sticky-cta` (the same geometry tokens, so it
   clears the nav the same way), and the emoji keyboard below.

   A comment is a *bubble* and a thread is the block they sit in: grouping
   consecutive messages from one person is something bubbles can show and a
   single card around a whole thread cannot. The replies keep their one indent
   and gain the rail that hangs from the message they answer, which is the
   only structure one level of threading buys.

   No new colour. The send button takes the accent because it is the page's
   one primary action, «حذف» takes the `--danger-text` it takes everywhere
   else, and everything else here is ink on the neutral fills.
   ========================================================================== */

/* --- the modal ----------------------------------------------------------- */
/* It comes up from the bottom, over whatever the reader was on. The scrim and
   the slide are `createSheet`'s own (`.sheet-backdrop`, `sheetSlideUp`), so
   the conversation opens the way every other overlay in this app opens; what
   it does differently is height — a sheet is as tall as its question, and a
   conversation is as tall as the screen allows. */
.cm-modal{position:fixed; inset:0; z-index:var(--z-sheet);}
/* The scrim is *this* modal's, so it is re-floored inside it: `.sheet-backdrop`
   carries the z-index that puts a sheet's scrim above the page, and left as
   it stands it would paint over the panel it belongs to. */
.cm-modal .sheet-backdrop{z-index:0;}
.cm-modal[hidden]{display:none;}
.cm-panel{
  position:absolute; z-index:1; inset-inline:0; bottom:0;
  margin-inline:auto; width:100%; max-width:var(--shell-max);
  height:88dvh;
  display:flex; flex-direction:column;
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  background:var(--bg-1); border:1px solid var(--border-strong); border-bottom:none;
  box-shadow:var(--shadow-lift);
  /* Fast: this is a reply to a tap, not an announcement. */
  animation:sheetSlideUp var(--dur-base) var(--ease);
}
/* The page behind must not scroll under a full-height overlay -- a phone
   otherwise scrolls it instead of the threads the moment a flick starts on
   the header. */
body.cm-modal-open{overflow:hidden;}

.cm-modal-head{
  position:relative; flex-shrink:0;
  display:flex; align-items:center; gap:var(--space-5);
  padding:var(--space-9) var(--space-8) var(--space-6);
  border-bottom:1px solid var(--border);
}
/* The mark that says «this came up and can go back down», centred over the
   header rather than taking a row of its own. */
.cm-grip{
  position:absolute; top:7px; inset-inline:0; margin-inline:auto;
  width:38px; height:4px; border-radius:var(--radius-xs); background:var(--fill-2);
}
.cm-modal-head h3{
  margin:0; min-width:0; flex:1;
  font-size:var(--fs-lg); font-weight:800;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cm-count{font-size:var(--fs-sm); color:var(--text-3); flex-shrink:0;}

/* The one scrolling region. Contained, or a flick past the last thread
   scrolls the page behind the modal. */
.cm-scroll{flex:1; overflow-y:auto; overscroll-behavior:contain; padding:var(--space-6) 0;}

/* A row that opens this modal rather than a page: a `<button>` wearing the
   leaderboard row's shape, so it needs the three things a button does not
   inherit. */
button.lb-entry{width:100%; font:inherit; text-align:start; cursor:pointer;}
/* The live count sits *inside* the row's second line, and `.lb-entry-text b`
   is a block (it is the row's title everywhere else). */
.lb-entry-text [data-comment-count]{display:inline;}

/* --- the list ------------------------------------------------------------ */
/* Two messenger conventions, and the geometry is all either of them is:
   consecutive messages from one person read as one turn (Telegram), and an
   answer steps in one level with the rail stepping in beside it (Instagram).
   Which rows those are is decided in `_comment_threads.html` -- only the
   template can see the row above -- so everything here keys off one class.

   Spacing is margins rather than a `gap`, because the two distances are the
   whole point: 12px says «somebody else is speaking», 2px says «still them».
   A gap can only say one of the two. */
.cm-list{display:flex; flex-direction:column; padding:0 var(--space-8);}
.cm-thread{margin-top:var(--space-6);}
.cm-thread:first-child{margin-top:0;}
.cm-thread.is-grouped{margin-top:var(--space-1);}

.cm{display:flex; gap:var(--space-4); align-items:flex-start;}
/* A grouped message keeps the avatar's *room* and not the avatar: without it
   the bubble slides back to the reading edge and the block stops being one
   person's turn. */
.cm .avatar.is-ghost{background:none;}

/* The column the bubble lives in. `align-items:flex-start` is what keeps a
   three-word comment three words wide instead of a full-width band. */
.cm-main{
  min-width:0; flex:1;
  display:flex; flex-direction:column; align-items:flex-start;
}

/* The bubble is the surface a comment now *is*, rather than a card holding a
   whole thread: grouping is a thing bubbles can show and a card cannot. The
   corner facing the avatar is cut short on every one of them, so a block of
   them reads as pointing at the same person. */
.cm-bubble{
  max-width:100%; padding:var(--space-4) var(--space-6);
  border-radius:var(--radius-md);
  border-start-start-radius:6px;
  background:var(--glass); border:1px solid var(--border);
}

.cm-head{display:flex; align-items:baseline; gap:var(--space-4); margin-bottom:1px;}
.cm-head b{
  font-size:var(--fs-md); font-weight:700; min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* It dates the turn, not each sentence in it, so it rides with the name and a
   grouped bubble carries none. */
.cm-when{font-size:var(--fs-xs); color:var(--text-3); flex-shrink:0;}
.cm-text{
  margin:0; font-size:var(--fs-lg); line-height:1.9;
  /* Line breaks the writer typed are line breaks, and a pasted URL wraps
     instead of widening the card. */
  white-space:pre-wrap; overflow-wrap:anywhere;
}
/* The rows written while a comment carried a sticker id of its own. Nothing
   writes one any more -- the keyboard types into the text. */
.cm-sticker{display:inline-block; font-size:var(--fs-4xl); line-height:1.3; margin-top:var(--space-1);}

.cm-actions{display:flex; gap:var(--space-7); margin-top:var(--space-2); padding-inline-start:var(--space-2);}
.cm-act{
  display:inline-flex; align-items:center; gap:var(--space-2);
  background:none; border:0; padding:var(--space-1);
  color:var(--text-3); font:inherit; font-size:var(--fs-sm); cursor:pointer;
  --press-scale:.9;
}
.cm-act svg{width:13px; height:13px;}
.cm-act.danger{color:var(--danger-text);}

/* The step in, and the rail that steps with it. Logical properties, not
   left/right: the rail runs down the reading edge and this page is RTL.
   `margin-inline-start` puts it under the middle of the root's 34px avatar,
   so the indent is not a number somebody chose -- it is a line dropped from
   the message being answered, past every answer to it. */
.cm-replies{
  display:flex; flex-direction:column;
  margin-inline-start:var(--space-9);
  padding-inline-start:var(--space-10);
  border-inline-start:1.5px solid var(--border);
}
.cm-reply{margin-top:var(--space-5);}
.cm-reply.is-grouped{margin-top:var(--space-1);}

.cm-reply .cm-text{font-size:var(--fs-md);}
.cm-reply .cm-bubble{background:var(--fill-1);}
.cm-more{
  align-self:flex-start; margin-top:var(--space-4); padding:var(--space-1);
  background:none; border:0; color:var(--fayrouzeh);
  font:inherit; font-size:var(--fs-sm); font-weight:700; cursor:pointer;
}

/* --- the composer -------------------------------------------------------- */
/* The foot of the panel: it no longer floats above the nav, because the modal
   already ends where the nav begins. It stays put while the threads scroll --
   the one thing this surface is for must not scroll away and ask the reader
   to find their place again. */
.cm-composer{
  flex-shrink:0;
  padding:var(--space-5) var(--space-6) calc(var(--space-5) + env(safe-area-inset-bottom));
  border-top:1px solid var(--border);
  background:var(--bg-1);
}
.cm-composer .btn-primary{margin-top:0; text-decoration:none;}

/* display:flex would beat the UA's [hidden]{display:none}. */
.cm-replying[hidden]{display:none;}
.cm-replying{
  display:flex; align-items:center; gap:var(--space-3);
  padding-bottom:var(--space-4); margin-bottom:var(--space-4);
  border-bottom:1px solid var(--border);
  font-size:var(--fs-sm); color:var(--text-3);
}
.cm-replying svg{width:14px; height:14px;}
.cm-replying b{color:var(--text-2); font-weight:700;}
.cm-replying .icon-btn{margin-inline-start:auto;}

.cm-input-row{display:flex; align-items:flex-end; gap:var(--space-4);}
.cm-input-row textarea{
  flex:1; min-width:0; resize:none;
  padding:var(--space-5) var(--space-7); border-radius:var(--radius-md);
  background:var(--fill-1); border:1px solid var(--border);
  color:var(--text-1); font:inherit; font-size:var(--fs-lg); line-height:1.7;
  max-height:140px; overflow-y:hidden;
}
.cm-input-row textarea:focus{outline:none; border-color:var(--border-strong);}
/* .btn-primary is full width by default; here it is a round send key. */
.cm-send{
  width:38px; height:38px; flex-shrink:0;
  padding:0; margin:0; border-radius:50%;
}
.cm-send:disabled{opacity:.45; box-shadow:none; cursor:default;}

/* ==========================================================================
   صفحه‌کلید اموجی
   --------------------------------------------------------------------------
   Laid out the way every phone keyboard already taught people to read one: a
   strip of category tabs, the open category named under it, the tiles below.
   The catalogue is `static/js/emoji.js`, so none of it is server-rendered and
   only the open tab is ever in the DOM.

   The tabs are monochrome stroke icons and the button that opens the panel
   carries no chrome at all: a coloured row above a grid of coloured emoji
   gives the eye nothing to land on, and a bordered circle beside the text
   field reads as a second thing to send with. Colour lands in exactly one
   place -- the accent on the open tab and on the open button, the same «this
   is the live one» it means everywhere else in the app.
   ========================================================================== */
.em-picker{
  padding-bottom:var(--space-5); margin-bottom:var(--space-5);
  border-bottom:1px solid var(--border);
}

.em-tabs{display:flex; gap:var(--space-1);}
.em-tab{
  flex:1; height:32px; display:grid; place-items:center;
  background:none; border:0; border-radius:var(--radius-sm);
  color:var(--text-3); cursor:pointer;
  transition:color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
  --press-scale:.88;
}
.em-tab svg{width:18px; height:18px;}
.em-tab.is-active{color:var(--fayrouzeh); background:rgba(var(--accent-rgb),0.12);}

.em-label{
  display:block; padding:var(--space-5) var(--space-2) var(--space-3);
  font-size:var(--fs-sm); font-weight:700; color:var(--text-3);
}

.em-grid{
  display:grid; grid-template-columns:repeat(auto-fill, minmax(38px,1fr));
  gap:var(--space-1); max-height:212px; overflow-y:auto;
  /* The list lives inside a fixed panel: without this, a flick past its end
     scrolls the conversation behind it. */
  overscroll-behavior:contain;
}
.em-tile{
  height:38px; padding:0; border:0; border-radius:var(--radius-sm);
  background:none; cursor:pointer;
  /* The device's own emoji font, named rather than left to the fallback: the
     app's Farsi stack carries no emoji, and what a bare fallback picks is a
     monochrome glyph on some Windows builds. */
  font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",
    "Twemoji Mozilla",sans-serif;
  font-size:var(--fs-3xl); line-height:1;
  --press-scale:.82;
}
.em-tile:hover{background:var(--fill-1);}
.em-empty{margin:var(--space-8) var(--space-3) var(--space-3); text-align:center; font-size:var(--fs-md); color:var(--text-3);}

/* The control that opens it: an outline glyph and nothing else. */
.btn-emoji{
  width:34px; height:34px; flex-shrink:0;
  color:var(--text-3);
  transition:color var(--dur-fast) var(--ease);
  --press-scale:.88;
}
.btn-emoji svg{width:22px; height:22px;}
.btn-emoji.is-open{color:var(--fayrouzeh);}

/* ==========================================================================
   «ریل» — the cover feed and its full-screen reader
   ==========================================================================
   The challenge list draws the *same* cards two ways, and the shape is the
   only state there is: `.rail` on the list container turns the row into a
   cover card, and `.is-immersive` on that same element turns the feed into a
   fixed, scroll-snapped reader. Nothing is re-fetched, re-rendered or copied
   into a second markup — which is what lets search, the filters, the filter
   sheet and infinite scroll carry on untouched in both modes, and what makes
   switching instant instead of a reload.

   Colour adds no channel here: status is the lamp in the cover's corner and
   nothing else, category keeps the `--cat-*` hue it already paints the thumb
   with, and the cover is that same hue at cover size.
   -------------------------------------------------------------------------- */

/* the view switch: two options, so it sizes to its content and sits at the
   end of its own row rather than stretching like a filter */
.seg-row.view-row{display:flex; justify-content:flex-end; padding-bottom:var(--space-5);}
.view-row .btn-seg{flex:0 0 auto; padding:var(--space-4) var(--space-7);}

/* --- the feed ----------------------------------------------------------- */
.challenge-list.rail{gap:var(--space-8);}
/* One cell, both children in it: the cover fills it and the body sits at its
   foot. A grid rather than the absolutely-positioned body it used to be,
   because that body was then the containing block for anything absolute
   inside it -- and the status pill has to be placed against the *card*, up on
   the cover, not against the strip of text at the bottom. */
.challenge-list.rail .challenge-card{
  display:grid; grid-template-columns:minmax(0,1fr); padding:0;
  /* the cover sits on a negative layer (below), which without a stacking
     context of its own here would sink beneath the card's glass background */
  isolation:isolate;
}

/* THE COVER. There is no image field yet, so the placeholder is the
   category's own gradient (inherited from [data-cat] .explore-thumb — the
   52px thumb and this are one rule), its glyph at cover size, and the app's
   own tile texture. When a picture lands it goes inside this element and
   nothing else here changes; a challenge without one keeps exactly this. */
.challenge-list.rail .explore-thumb{
  grid-area:1/1;
  /* Below the body rather than beside it: the two share one grid cell, and a
     negative layer is what lets the body stay in normal flow -- which in turn
     is what leaves the *card* as the containing block for the status pill, up
     on the cover. Its texture and scrim ride inside this same layer. */
  z-index:-1;
  width:100%; height:auto; aspect-ratio:4/3; border-radius:0;
}
/* No picture: nothing is drawn at all except a flat 20% wash in the
   category's own hue and the glyph on top of it. No gradient, no tile
   texture, no scrim -- the card is a sheet of tinted glass and the page
   ground behind it reads through at the remaining 80%. */
.challenge-list.rail .explore-thumb:not(.has-cover){
  background:linear-gradient(145deg,
    rgba(var(--cat-rgb, var(--accent-rgb)),0.12), rgba(var(--cat-rgb, var(--accent-rgb)),0));
}
/* ...which only works if the pane itself stops painting. `.glass` puts an
   opaque-ish sheet under every card; a coverless rail card drops it and keeps
   the rest of the primitive (hairline, sheen, blur), so the one paint left on
   the card is the tint above. The blur stays: transparent is not the same as
   absent, and the glass is what keeps the body's text legible over whatever
   the ground happens to be. */
.challenge-list.rail .challenge-card:has(.explore-thumb:not(.has-cover)){
  background:transparent;
  /* a 20px blur is itself opaque-ish -- it erases the ground it is meant to
     show -- so a coverless card keeps only a whisper of it */
  backdrop-filter:blur(1.4px); -webkit-backdrop-filter:blur(1.4px);
  /* with no pane behind it the edge is the whole object, so it is lit twice:
     the sheen along the top and a hairline of the same light all round,
     bleeding a little outside -- glass catching light, not a neon tube */
  border-color:color-mix(in srgb, var(--glass-sheen) 45%, var(--border));
  box-shadow:inset 0 1px 0 var(--glass-sheen),
             inset 0 0 12px -6px var(--glass-sheen),
             0 0 10px -4px var(--glass-sheen);
}
.challenge-list.rail .explore-thumb:not(.has-cover)::before{display:none;}
/* no photo behind it, so the base rail scrim (meant to carry text over a
   variable-luminance picture) is dropped -- it read as an opaque dark box
   sitting on top of the 20% tint and swallowed it entirely */
.challenge-list.rail .explore-thumb:not(.has-cover)::after{background:none;}
.challenge-list.rail .explore-thumb > [data-icon]:not(.badge-cadence) svg{
  width:60px; height:60px; opacity:.9;
}
/* the same tile the page ground wears, so the placeholder reads as part of
   this app rather than as a missing image */
.challenge-list.rail .explore-thumb::before{
  content:""; position:absolute; inset:0;
  background-image:var(--tile-url); background-size:84px; opacity:.22;
}
/* the cover has to carry text at its foot, and later a photograph will.
   Fading to the raised ground rather than to black keeps the body's own
   --text-* ink legible in both themes without a second palette. */
.challenge-list.rail .explore-thumb::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(to top, var(--bg-2) 4%, transparent 60%);
}
/* the 52px thumb hangs this badge off its corner; on a full-width cover
   that corner is clipped, so it moves inside the cover's own top edge */
.challenge-list.rail .badge-cadence{
  inset-block-end:auto; inset-block-start:12px; inset-inline-start:12px;
  width:26px; height:26px;
}
.challenge-list.rail .badge-cadence svg{width:13px; height:13px;}

/* The body is the same grid cell as the cover, parked at its foot. Its own
   rows are laid out here rather than by `.explore-head`, which goes
   `display:contents` just below: the title wants a line of its own, and the
   heart belongs on the meta line beside the category -- one action sitting on
   the row of facts it is about, rather than floating above it. */
.challenge-list.rail .explore-body{
  grid-area:1/1; align-self:end;
  display:flex; flex-wrap:wrap; align-items:center;
  column-gap:var(--space-5); row-gap:var(--space-3);
  padding:var(--space-6) var(--space-8) var(--space-7);
}
/* `.explore-head` is the list row's title/side split and has nothing to say
   here; dissolving it lets its children take part in the body's own rows. */
.challenge-list.rail .explore-head,
.challenge-list.rail .ec-side,
.challenge-list.rail .ec-acts{display:contents;}
/* `display:contents` changes layout, not the tree, so these stay descendant
   selectors -- a `>` from the body would match neither the title nor the
   heart, both of which are still children of `.explore-head`. */
.challenge-list.rail .explore-body h3{
  order:1; flex:0 0 100%; margin:0;
  font-size:var(--fs-xl); line-height:1.7; white-space:normal;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.challenge-list.rail .explore-body > .cc-group{order:2; flex:0 0 100%;}
/* the facts line: category, member count, due date */
.challenge-list.rail .explore-body > p:not(.cc-group){
  order:3; flex:1 1 auto; min-width:0;
}
/* the heart and the conversation ride at the far end of that same line --
   the actions sitting on the row of facts they are about, rather than
   stacked above the title where they competed with the status pill */
.challenge-list.rail .explore-body .btn-social{
  order:4; flex:0 0 auto; align-self:center;
}
.challenge-list.rail .explore-body .btn-like{margin-inline-start:auto;}
.challenge-list.rail .explore-body > .rail-extra{order:5; flex:0 0 100%;}

/* THE LAMP, up on the cover's top corner: it is the first question a reader
   asks of a full-bleed card and it must not sit in the same breath as the
   title. `.ec-side` goes `display:contents` here, so it generates no box and
   the lamp's containing block falls through to the card itself -- which is
   why the list row's reserved padding and its opaque open ground both
   disappear on their own in this mode.

   Over a photograph the lamp needs a ground of its own: a colour alone is
   not legible on a bright cover, and the frost is what separates it from
   whatever was photographed underneath. It is carried at rest as well as
   open, because the ring is the whole signal here. */
/* --- the lamp over a picture ---------------------------------------------
   Two surfaces, one rule: the rail card's cover and the detail hero's own
   ground. A neutral frost alone was a mistake here -- against a cover
   painted in the category's hue, a dark disc with a 6px dot in it reads as
   *the cover's* colour with a speck on top, which is the one thing this
   channel must never do. So the frost carries the status hue itself: the
   whole disc is green for «در حال اجرا», amber for «شروع نشده», grey for
   «تمام‌شده», and the ring's hairline is drawn at full strength rather than
   the 42% that is enough on a flat card. */
.challenge-list.rail .st-lamp,
.detail-hero > .st-lamp{
  position:absolute; z-index:3;
  top:12px; left:12px;
  padding:0 5px;
  background:
    linear-gradient(rgba(var(--status-rgb),.28), rgba(var(--status-rgb),.28)),
    rgba(var(--lamp-ground-rgb),.32);
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  box-shadow:0 2px 10px rgba(0,0,0,.18);
}
.challenge-list.rail .st-lamp .ring,
.detail-hero > .st-lamp .ring{border-color:rgb(var(--status-rgb));}
.challenge-list.rail .st-lamp.is-open,
.detail-hero > .st-lamp.is-open{
  padding:0 9px 0 5px;
  background:
    linear-gradient(rgba(var(--status-rgb),.28), rgba(var(--status-rgb),.28)),
    rgba(var(--lamp-ground-rgb),.32);
}

/* --- the full-screen reader --------------------------------------------- */
/* One panel per card, snapped hard: `mandatory` plus `scroll-snap-stop` is
   what guarantees the reader is never parked between two challenges, and a
   fling can never skip one. Above the nav (30) and the FAB (40), below the
   toast (60) so a failed like still says so. */
.challenge-list.is-immersive{
  position:fixed; inset:0; z-index:var(--z-immersive);
  padding:0; gap:0; margin:0;
  background:transparent;
  overflow-y:auto; overscroll-behavior:contain;
  scroll-snap-type:y mandatory;
  /* the reader is a sheet of glass over the page it opened from, not a second
     opaque ground -- otherwise a coverless card's 4% tint has nothing behind
     it to show through to */
  backdrop-filter:blur(1.4px); -webkit-backdrop-filter:blur(1.4px);
  /* the panels are the only scroll indicator this view needs */
  scrollbar-width:none;
}
.challenge-list.is-immersive::-webkit-scrollbar{display:none;}
.challenge-list.is-immersive .challenge-card{
  flex:0 0 auto;
  width:100%; max-width:var(--shell-max); margin-inline:auto;
  height:100vh; height:100dvh;
  border-radius:0; border-block:0;
  scroll-snap-align:start; scroll-snap-stop:always;
}
.challenge-list.is-immersive .explore-thumb{
  position:absolute; inset:0; width:auto; height:auto; aspect-ratio:auto;
}
.challenge-list.is-immersive .explore-thumb > [data-icon]:not(.badge-cadence) svg{
  width:96px; height:96px;
}
.challenge-list.is-immersive .explore-thumb::after{
  background:linear-gradient(to top, var(--bg-2) 16%, transparent 76%);
}
/* the panel's text can outgrow its share of the screen once the description
   is opened, so it scrolls inside itself — contained, or that scroll would
   chain into the snap container and move to the next challenge */
.challenge-list.is-immersive .explore-body{
  padding:var(--space-9) var(--space-10) calc(var(--space-9) + env(safe-area-inset-bottom));
  max-height:78%; overflow-y:auto; overscroll-behavior:contain;
}
.challenge-list.is-immersive .explore-body h3{font-size:var(--fs-2xl); -webkit-line-clamp:3;}
/* The reader has one vertical line down its left edge -- the close button at
   the top, the heart and the conversation at the foot, all standing on
   `.explore-body`'s own 20px gutter. The lamp joins it rather than floating
   at an offset of its own: `left` is that gutter less the 5px the frosted
   lamp carries as padding, so it is the *ring* that lands on the line, and
   `top` clears the close button (14px + its 40px) rather than sitting beside
   it. Three marks in one column, which is what «aligned» means here. */
.challenge-list.is-immersive .st-lamp{
  top:calc(66px + env(safe-area-inset-top));
  left:calc(var(--space-10) - 5px);
}

/* what the reader adds to the card, and the only thing it adds */
.rail-extra{display:none;}
.challenge-list.is-immersive .rail-extra{display:block; margin-top:var(--space-5);}
.explore-body .rail-tags{display:flex; flex-wrap:wrap; gap:var(--space-3); margin:0 0 var(--space-4);}
/* .explore-body p is a flex row at 11.5px; the description is neither.
   Collapsed and full ship as two spans and «بیشتر» swaps them (see the
   partial): a line-clamp cannot be toggled meaningfully, because a
   description already short enough to fit is clamped to nothing and the
   control then appears to do nothing at all. */
.explore-body .rail-desc{
  display:block; margin:0; cursor:pointer;
  font-size:var(--fs-md); line-height:1.9; color:var(--text-2);
}
.rd-full{display:none;}
.challenge-card.desc-open .rd-short{display:none;}
/* Open, a long description scrolls inside itself rather than pushing the
   card's controls off the panel -- contained, or that scroll chains into the
   snap container and moves to the next challenge. */
.challenge-card.desc-open .rd-full{
  display:block; max-height:50vh;
  overflow-y:auto; overscroll-behavior:contain;
}

/* .icon-btn is display:grid, which beats the UA's [hidden]{display:none} --
   the same trap .list-loading documents above. */
.icon-btn.rail-close[hidden]{display:none;}
.icon-btn.rail-close{
  position:fixed; z-index:calc(var(--z-immersive) + 1);
  inset-block-start:calc(14px + env(safe-area-inset-top)); inset-inline-end:16px;
  background:var(--chrome-bg-strong); color:var(--text-1);
  backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px);
}
/* ==========================================================================
   مسیر | a course made of challenges
   --------------------------------------------------------------------------
   Built almost entirely out of primitives that already carry meaning
   elsewhere -- .glass, .detail-hero and its picture, .setting-row, the ring,
   .pill -- so a shape a member learned on the challenge or group screens
   means the same thing here.

   **Colour gains no channel.** Status owns --st-* and category owns --cat-*,
   and a step's state is not a third meaning riding on either: it is shown by
   the node's shape, its glyph and its words. The one exception is the accent
   on the step that is actually open, and that is not a new meaning at all --
   the accent has meant «this one is live» everywhere in this app since the
   first screen.
   ========================================================================== */

/* --- the roadmap card (roadmap/_roadmap_cards.html) --- */
.rm-list{display:flex; flex-direction:column; gap:var(--space-5);}
.rm-card{
  display:flex; align-items:center; gap:var(--space-6); padding:var(--space-6);
  border-radius:var(--radius-md); text-decoration:none; color:inherit;
  --press-scale:.985;
}
/* The cover: a picture when there is one, and the route glyph on an
   accent-tinted ground when there is not. A placeholder that belongs to the
   thing rather than a grey box -- the same trade .explore-thumb makes. */
.rm-cover{
  position:relative; flex:none; width:52px; height:52px; overflow:hidden;
  border-radius:var(--radius-sm); display:grid; place-items:center;
  background:rgba(var(--accent-rgb),0.14); color:var(--fayrouzeh);
  box-shadow:inset 0 0 0 1px rgba(var(--accent-rgb),0.22);
}
.rm-cover svg{width:22px; height:22px;}
.rm-cover img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;}
/* With a picture the glyph steps back to a corner mark, so the cover is the
   photograph and not a photograph with a symbol stamped on it. */
.rm-cover.has-cover svg{
  position:absolute; inset-block-end:3px; inset-inline-end:3px;
  width:13px; height:13px; color:var(--on-accent);
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.55));
}
.rm-cover.lg{width:84px; height:84px; margin:0 auto var(--space-7); border-radius:var(--radius-md);}
.rm-cover.lg svg{width:34px; height:34px;}

.rm-body{flex:1; min-width:0;}
.rm-body h3{
  margin:0 0 var(--space-2); font-size:var(--fs-lg); font-weight:800; color:var(--text-1);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.rm-meta{
  margin:0; display:flex; align-items:center; flex-wrap:wrap; gap:var(--space-2) var(--space-3);
  font-size:var(--fs-sm); color:var(--text-3);
}
.rm-meta svg{width:13px; height:13px;}
.rm-meta span + span{margin-inline-end:var(--space-3);}
.rm-tags{margin:var(--space-3) 0 0; display:flex; flex-wrap:wrap; gap:var(--space-3);}
.rm-tags:empty{display:none;}
.rm-chev{flex:none; color:var(--text-3);}
.rm-chev svg{width:16px; height:16px;}
/* The one place the accent appears in this list, meaning what it always
   means: this is the live one. */
.rm-card.is-walking{box-shadow:inset 2px 0 0 var(--fayrouzeh), var(--shadow-soft);}

/* --- the course's hero --- */
.rm-mark{
  position:relative; z-index:1; width:56px; height:56px; margin:0 auto var(--space-6);
  border-radius:var(--radius-md); display:grid; place-items:center;
  background:rgba(var(--accent-rgb),0.16); color:var(--fayrouzeh);
  box-shadow:inset 0 0 0 1px rgba(var(--accent-rgb),0.28);
}
.rm-mark svg{width:26px; height:26px;}
.rm-owner{
  position:relative; z-index:1; margin:0 0 var(--space-5);
  font-size:var(--fs-md); color:var(--text-3);
}

/* --- the track: what makes this a course and not a list -------------------
   One node per step on a single line, so the eye reads the order before it
   reads any title. The connector is drawn by each step's ::before rather than
   by a background on the list, so it stops correctly at the last node
   whatever the count. */
.rm-track{list-style:none; margin:0; padding:0 var(--space-10);}
.rm-step{position:relative; padding-inline-start:38px; padding-bottom:var(--space-6);}
.rm-step::before{
  content:""; position:absolute; inset-block-start:26px; inset-block-end:0;
  inset-inline-start:13px; width:2px; border-radius:var(--radius-xs); background:var(--track);
}
.rm-step:last-child::before{display:none;}
/* A settled step's connector is filled: the line itself becomes the progress
   bar of the whole course, at no extra element. */
.rm-step.is-completed::before,
.rm-step.is-skipped::before{background:rgba(var(--accent-rgb),0.45);}

.rs-node{
  position:absolute; inset-inline-start:0; inset-block-start:2px;
  width:28px; height:28px; border-radius:50%;
  display:grid; place-items:center; z-index:1;
  font-size:var(--fs-md); font-weight:800; font-variant-numeric:tabular-nums;
  background:var(--fill-2); color:var(--text-3);
  box-shadow:inset 0 0 0 1px var(--border-strong);
}
.rs-node svg{width:15px; height:15px;}
.rm-step.is-available .rs-node,
.rm-step.is-in_progress .rs-node{
  background:linear-gradient(145deg, var(--fayrouzeh), var(--fayrouzeh-dim));
  color:var(--on-accent); box-shadow:var(--glow-accent);
}
.rm-step.is-completed .rs-node{
  background:rgba(var(--accent-rgb),0.18); color:var(--fayrouzeh);
  box-shadow:inset 0 0 0 1px rgba(var(--accent-rgb),0.42);
}
/* Skipped is settled but not achieved, so it takes the neutral outline the
   check-in legend already uses for the same idea rather than a warning hue. */
.rm-step.is-skipped .rs-node{
  background:var(--fill-mute); color:var(--text-3);
  box-shadow:inset 0 0 0 1px var(--border-strong);
}
/* The compact node the management screen's step rows carry. */
.rs-node.sm{position:static; width:24px; height:24px; font-size:var(--fs-sm); flex:none;}

.rs-card{padding:var(--space-6) var(--space-7); border-radius:var(--radius-md);}
.rm-step.is-locked .rs-card{opacity:.72;}
.rs-head{
  display:flex; align-items:center; justify-content:space-between; gap:var(--space-4);
  margin-bottom:var(--space-3);
}
.rs-num{font-size:var(--fs-sm); color:var(--text-3); font-weight:700;}
.rs-state{
  display:inline-flex; align-items:center; gap:var(--space-2);
  font-size:var(--fs-xs); font-weight:700; color:var(--text-3);
  padding:var(--space-2) var(--space-4); border-radius:var(--radius-pill);
  background:var(--fill-1); border:1px solid var(--border);
}
.rs-state svg{width:12px; height:12px;}
.rm-step.is-available .rs-state,
.rm-step.is-in_progress .rs-state{
  color:var(--fayrouzeh); background:rgba(var(--accent-rgb),0.12);
  border-color:rgba(var(--accent-rgb),0.34);
}
.rs-card h3{margin:0 0 var(--space-3); font-size:var(--fs-lg); font-weight:800; line-height:1.6;}
.rs-card h3 a{color:var(--text-1); text-decoration:none;}
.rs-hidden{display:inline-flex; align-items:center; gap:var(--space-3); color:var(--text-3);}
.rs-hidden svg{width:14px; height:14px;}

.rs-meta{margin:0; display:flex; flex-direction:column; gap:var(--space-2);}
.rs-line{
  display:flex; align-items:center; gap:var(--space-3);
  font-size:var(--fs-sm); color:var(--text-2); line-height:1.9;
}
.rs-line svg{width:13px; height:13px; flex:none; color:var(--text-3);}
.rs-note{
  margin:var(--space-4) 0 0; padding-inline-start:var(--space-5); font-size:var(--fs-sm); line-height:1.9;
  color:var(--text-3); border-inline-start:2px solid var(--border-strong);
}

/* The share, at the width of the card. A filled track means one thing across
   the app, so this is the bar the collective-progress row already uses. */
.rs-bar{
  margin-top:var(--space-5); height:6px; border-radius:var(--radius-pill);
  background:var(--track); overflow:hidden;
}
.rs-bar > span{
  display:block; height:100%; border-radius:inherit;
  background:linear-gradient(90deg, var(--fayrouzeh), var(--fayrouzeh-dim));
}
.rs-count{
  margin:var(--space-3) 0 0; font-size:var(--fs-xs); color:var(--text-3);
  font-variant-numeric:tabular-nums;
}

.rs-actions{
  display:flex; align-items:center; flex-wrap:wrap; gap:var(--space-4); margin-top:var(--space-5);
}
.rs-actions:empty{display:none;}
.rs-wait{
  flex:1 1 100%; margin:0; display:flex; align-items:center; gap:var(--space-3);
  font-size:var(--fs-sm); color:var(--text-3); line-height:1.8;
}
.rs-wait svg{width:13px; height:13px; flex:none;}
.rs-go{
  display:inline-flex; align-items:center; gap:var(--space-3);
  font-size:var(--fs-md); font-weight:700; color:var(--fayrouzeh); text-decoration:none;
}
.rs-go svg{width:14px; height:14px;}
.rs-done{
  display:inline-flex; align-items:center; gap:var(--space-3); cursor:pointer;
  padding:var(--space-4) var(--space-6); border-radius:var(--radius-sm);
  border:1px solid rgba(var(--accent-rgb),0.4);
  background:rgba(var(--accent-rgb),0.14); color:var(--fayrouzeh);
  font-family:inherit; font-size:var(--fs-md); font-weight:800;
}
.rs-done svg{width:14px; height:14px;}
.rs-done:disabled{opacity:.5; cursor:default;}

/* --- the «من» panel --- */
.rm-current-title{margin:0 0 var(--space-2); font-size:var(--fs-lg); font-weight:800;}
.rm-current-rule{margin:0 0 var(--space-6); font-size:var(--fs-md); color:var(--text-2);}
.rm-finished{text-align:center;}
.rm-finished svg{width:34px; height:34px; color:var(--gold);}
.rm-finished p{margin:var(--space-4) 0 0; font-size:var(--fs-lg); font-weight:700;}

/* --- the management screen's step rows --- */
.rm-step-row .sr-head{align-items:center; gap:var(--space-5);}
.rm-step-row .sr-actions{gap:var(--space-1);}
.icon-btn.xs:disabled{opacity:.32; cursor:default;}

/* --- the challenge picker (roadmap/manage.html) ---
   Its own panel rather than a createSheet field, because picking a challenge
   is a search and a sheet's fields are a static form. It borrows the sheet's
   scrim, panel and slide, so it is the same object the rest of the app
   opens. */
/* `.sheet` is `display:flex` and `.sheet-backdrop` is `display:block`, and a
   class rule beats the UA's `[hidden]{display:none}` -- the same trap
   `.list-loading` and `.icon-btn.rail-close` document above. Every sheet
   `createSheet` builds is added and removed from the DOM, so this only ever
   matters for one that ships in the markup and toggles `hidden`: the
   roadmap's challenge picker. */
.sheet[hidden], .sheet-backdrop[hidden]{display:none;}

.rm-pick-list{display:flex; flex-direction:column; gap:var(--space-3); max-height:46vh; overflow-y:auto;}
.rm-pick-item{
  display:flex; align-items:baseline; gap:var(--space-4); text-align:start; cursor:pointer;
  padding:var(--space-6) var(--space-6); border-radius:var(--radius-sm);
  border:1px solid var(--border); background:var(--fill-1);
  color:var(--text-1); font-family:inherit;
}
.rm-pick-item b{font-size:var(--fs-lg); font-weight:700; flex:1; min-width:0;}
.rm-pick-item span{font-size:var(--fs-xs); color:var(--text-3); flex:none;}

/* --- «قدم فعلی تو» on the home dashboard --- */
.rms-list{display:flex; flex-direction:column; gap:var(--space-4);}
.rms-row{padding:var(--space-6) var(--space-7); border-radius:var(--radius-md);}
.rms-course{
  display:inline-flex; align-items:center; gap:var(--space-3);
  font-size:var(--fs-sm); font-weight:700; color:var(--text-3); text-decoration:none;
}
.rms-course svg{width:12px; height:12px;}
.rms-title{
  display:flex; align-items:center; gap:var(--space-3); margin:var(--space-3) 0 0;
  font-size:var(--fs-lg); font-weight:800; color:var(--text-1); text-decoration:none;
}
.rms-title.is-hidden{color:var(--text-3); font-weight:700;}
.rms-title svg{width:14px; height:14px;}
.rms-chev{margin-inline-start:auto; color:var(--text-3);}
.rms-rule{margin:var(--space-2) 0 0; font-size:var(--fs-sm); color:var(--text-2);}

/* --- the invite landing's dead-link line --- */
.invite-dead{
  display:flex; align-items:center; justify-content:center; gap:var(--space-4);
  margin:var(--space-8) 0 var(--space-4); font-size:var(--fs-md); color:var(--danger-text);
}
.invite-dead svg{width:15px; height:15px;}

/* ==========================================================================
   ستون گوشی روی صفحه‌های عریض | The phone column on wide screens
   ==========================================================================
   Above the breakpoint the app does not stretch and it does not grow a second
   layout: it stays the same phone, rendered in one column of --shell-max,
   centred, with the app's own ambient ground carrying on around it. Below the
   breakpoint nothing in this section applies and the phone is untouched.

   Three facts hold the whole section up.

   1. **The column is the scroll container, not the page.** `.shell` goes
      `position:fixed` at full viewport height and scrolls inside itself, so
      the window never scrolls and a `position:fixed` overlay's coordinates
      keep meaning what they say on screen.

   2. **`.shell` deliberately takes no `transform`, `filter`, `contain` or
      `will-change`.** Any one of those would make it the containing block for
      its own fixed descendants — and since it is also the scroller, the
      bottom nav would then scroll away with the content. That is why the
      column is centred with `left/right:0` + `margin-inline:auto` rather than
      the `translateX(-50%)` the rest of this file uses, and why every fixed
      overlay is clamped by hand below instead of inherited for free.

   3. **--shell-gutter is the seam.** It is the distance from a viewport edge
      to the column's edge, so anything that used to be `inset:0` now stops at
      the column by reading it. Anything already centred on --shell-max (the
      nav, the toast, the sticky CTA, every sheet panel, the FAB's own calc)
      needs nothing here, and anything placed by JS in viewport coordinates
      (the tour's ring and callout, the explainer popover) is clamped in JS
      against `shellBounds()` in app.js.

   Before adding a `position:fixed` rule anywhere in this file, add it to the
   list below or satisfy yourself that it is already column-bound: a missed
   overlay is invisible on a phone and silently wrong here.
   ========================================================================== */
/* The breakpoint is 600px and the column keeps --shell-max at its existing
   480, both for the same reason: crossing this line must not resize the app.
   The widest phone in portrait is ~440 CSS px and .shell is already capped at
   480 below here, so a viewport that reaches 600 is not a phone, and the app
   it draws at 599 is the same width as the one it draws at 601 -- only the
   ground around it, the full height and the self-scrolling change. A narrower
   column (430 was the first try) is a visible 50px jump at the seam and buys
   nothing: 480 is the width every screen in this app was drawn against. */
@media (min-width:600px){
  :root{
    --shell-gutter:max(0px, (100vw - var(--shell-max)) / 2);
  }

  /* the window itself never scrolls — the column does */
  html, body{height:100%; overflow:hidden;}

  .shell{
    position:fixed; inset-block:0; left:0; right:0;
    width:min(var(--shell-max), 100vw); margin-inline:auto;
    min-height:0; height:100%;
    overflow-y:auto; overflow-x:hidden;
    overscroll-behavior:contain;
    /* the column is narrow and a desktop-width scrollbar eats a visible slice
       of it; thin keeps the affordance a pointer still needs */
    scrollbar-width:thin;
    /* the soft edge asked for: the app's own hairline and lift shadow, the
       same pair every glass pane in it wears. Not a device frame — the
       column is a surface of this app, not a picture of a handset. */
    border-inline:1px solid var(--border);
    box-shadow:var(--shadow-lift);
  }
  /* a sheet's scroll lock writes `overflow:hidden` onto <body>, which is
     already hidden here, so the lock has to reach the real scroller */
  body.is-scroll-locked .shell{overflow:hidden;}

  /* The ground outside the column is the app's own ambient layer, dimmed a
     step by --scrim so the column reads as lifted off it. It is drawn as a
     third layer on .app-bg rather than a second background stack, so the
     glows and the tile are declared once and stay continuous across the seam
     — the tile runs through the gutters and through the column alike, and
     only the gutters are darkened. */
  .app-bg::after{
    content:"";
    position:absolute; inset:0; pointer-events:none;
    background:linear-gradient(90deg,
      var(--scrim) 0 var(--shell-gutter),
      transparent var(--shell-gutter) calc(100% - var(--shell-gutter)),
      var(--scrim) calc(100% - var(--shell-gutter)) 100%);
  }

  /* --- every full-bleed fixed overlay, clamped to the column --------------
     Each of these is `inset:0` (or pinned to a viewport edge) where it is
     declared above, which on a wide window means the whole screen. The panel
     *inside* each is already centred on --shell-max, so what this fixes is
     the scrim, the veil and the reader spilling out around it. */
  .sheet-backdrop,                    /* createSheet's scrim */
  .sheet,                             /* and its positioning layer */
  .ip-modal,                          /* the image cropper */
  .cm-modal,                          /* the comments modal */
  .tour,                              /* the tour's root */
  .tour-veil,                         /* and its veil (see tour.js place()) */
  .challenge-list.is-immersive{       /* «ریل»'s full-screen reader */
    left:var(--shell-gutter); right:var(--shell-gutter);
  }
  /* the two that hang off an edge by a fixed offset rather than filling it */
  .floating-back{inset-inline-start:calc(var(--shell-gutter) + 18px);}
  .icon-btn.rail-close{inset-inline-end:calc(var(--shell-gutter) + 16px);}
}
