
  /* ===== PALETTES =====
     Each palette pairs an accent hue with a separate surface hue (and a different
     surface for dark), so the two colours read as a matched pair rather than one tint.
     Generated + machine-verified: text >=4.5:1, control borders / bar fills >=3:1,
     across all 22 palettes x light+dark. */
  :root{
    color-scheme:dark;
    --bg:#0e101c;
    --panel:#181a2c;
    --panel2:#222539;
    --line:#3b3f56;
    --line-strong:#696e8c;
    --text:#eff0f3;
    --muted:#898ca2;
    --brand:#e66842;
    --brand2:#e7876a;
    --on-brand:#0e0f18;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ed5a6d;
    --violet:#ac71ef;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ed5a6d;
    --header:rgba(14,16,28,.86);
    --overlay:rgba(5,6,10,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1c1f31 8%,#2c304c 22%,#1c1f31 36%);
    --s-plan-bg:#402118;
    --s-plan-fg:#e86943;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2c2f42;
    --s-leave-fg:#9296ac;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(230,104,66,.35);
    /* liquid-glass tokens: top specular highlight, bottom shade, combined inset,
       soft drop shadow (dark keeps depth from the inset alone), surface alphas,
       ambient-orb strength */
    --glass-hi:rgba(255,255,255,.10);
    --glass-lo:rgba(0,0,0,.30);
    --glass-in:inset 0 1px 0 var(--glass-hi),inset 0 -1px 0 var(--glass-lo);
    --glass-drop:0 0 0 transparent;
    --glass-a:55%;   /* small surfaces: rows, cells, inputs, controls */
    --glass-a2:62%;  /* mid surfaces: cards, kpis, pop, header */
    /* 97, not 72. A sheet at 72% let the page behind it show THROUGH the
       panel — measured at 19/255 of channel difference inside a badge tile,
       which is Thai text you can read off the shelf. The backdrop blur was
       working; 28% of a blurred page is simply still a page. At 97% the same
       measurement is 2/255, under what an eye resolves on a dark surface.
       The LIGHT theme has been on 97% all along (see below) and never had
       this — the two now agree. */
    --glass-a3:97%;  /* strong surfaces: sheet, toast */
    --orb-a:15%;--orb-b:12%;--orb-c:9%;
  }
  [data-theme="light"]{
    color-scheme:light;
    --bg:#f6efe7;
    --panel:#fefefe;
    --panel2:#f0e7db;
    --line:#e4d9cb;
    --line-strong:#958167;
    --text:#282118;
    --muted:#78664f;
    --brand:#b75c40;
    --brand2:#a55036;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#8e5e0b;
    --red:#d0112a;
    --violet:#7312e2;
    --green-solid:#0f9949;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(246,239,231,.88);
    --overlay:rgba(39,32,22,.45);
    --shadow:0 10px 30px rgba(67,53,35,.13);
    --shadow-sm:0 2px 8px rgba(67,53,35,.09);
    --skel:linear-gradient(90deg,#ede3d6 8%,#f8f6f4 22%,#ede3d6 36%);
    --s-plan-bg:#f3e7e3;
    --s-plan-fg:#aa4d31;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#ebe3d8;
    --s-leave-fg:#76634a;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(183,92,64,.28);
    --glass-hi:rgba(255,255,255,.95);
    --glass-lo:rgba(100,85,55,.16);
    --glass-drop:0 1px 2px rgba(110,90,60,.05),0 10px 24px rgba(110,90,60,.08);
    --glass-a:92%;--glass-a2:95%;--glass-a3:97%;
    --ctl:color-mix(in srgb,var(--panel2) 34%,var(--panel));
    --orb-a:4%;--orb-b:3%;--orb-c:2%;
  }
  [data-palette="vintage"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0e101c;
    --panel:#181a2c;
    --panel2:#222539;
    --line:#3b3f56;
    --line-strong:#696e8c;
    --text:#eff0f3;
    --muted:#898ca2;
    --brand:#e66842;
    --brand2:#e7876a;
    --on-brand:#0e0f18;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ed5a6d;
    --violet:#ac71ef;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ed5a6d;
    --header:rgba(14,16,28,.86);
    --overlay:rgba(5,6,10,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1c1f31 8%,#2c304c 22%,#1c1f31 36%);
    --s-plan-bg:#402118;
    --s-plan-fg:#e86943;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2c2f42;
    --s-leave-fg:#9296ac;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(230,104,66,.35);
  }
  [data-palette="vintage"][data-theme="light"]{
    color-scheme:light;
    --bg:#f6efe7;
    --panel:#fefefe;
    --panel2:#f0e7db;
    --line:#e4d9cb;
    --line-strong:#958167;
    --text:#282118;
    --muted:#78664f;
    --brand:#b75c40;
    --brand2:#a55036;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#8e5e0b;
    --red:#d0112a;
    --violet:#7312e2;
    --green-solid:#0f9949;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(246,239,231,.88);
    --overlay:rgba(39,32,22,.45);
    --shadow:0 10px 30px rgba(67,53,35,.13);
    --shadow-sm:0 2px 8px rgba(67,53,35,.09);
    --skel:linear-gradient(90deg,#ede3d6 8%,#f8f6f4 22%,#ede3d6 36%);
    --s-plan-bg:#f3e7e3;
    --s-plan-fg:#aa4d31;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#ebe3d8;
    --s-leave-fg:#76634a;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(183,92,64,.28);
  }
  [data-palette="sage"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0f1b18;
    --panel:#192b26;
    --panel2:#243833;
    --line:#3d554f;
    --line-strong:#65837c;
    --text:#eff3f2;
    --muted:#8ba19c;
    --brand:#52d698;
    --brand2:#77daac;
    --on-brand:#0f1816;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#f07888;
    --violet:#b885f1;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#f07888;
    --header:rgba(15,27,24,.86);
    --overlay:rgba(5,10,9,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1d2f2b 8%,#2e4a44 22%,#1d2f2b 36%);
    --s-plan-bg:#18402d;
    --s-plan-fg:#40e799;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2d413c;
    --s-leave-fg:#95aca7;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(82,214,152,.35);
  }
  [data-palette="sage"][data-theme="light"]{
    color-scheme:light;
    --bg:#f4f0e9;
    --panel:#fefefe;
    --panel2:#ede8de;
    --line:#e1dbce;
    --line-strong:#8f846d;
    --text:#26221a;
    --muted:#716753;
    --brand:#3d8262;
    --brand2:#347456;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#8e5e0b;
    --red:#d0112a;
    --violet:#7312e2;
    --green-solid:#0f9949;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(244,240,233,.88);
    --overlay:rgba(37,33,24,.45);
    --shadow:0 10px 30px rgba(63,55,39,.13);
    --shadow-sm:0 2px 8px rgba(63,55,39,.09);
    --skel:linear-gradient(90deg,#eae4d9 8%,#f8f7f4 22%,#eae4d9 36%);
    --s-plan-bg:#e4f2ec;
    --s-plan-fg:#247b52;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e9e4db;
    --s-leave-fg:#70654f;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(61,130,98,.28);
  }
  [data-palette="clay"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1b120f;
    --panel:#2b1e19;
    --panel2:#382924;
    --line:#55433d;
    --line-strong:#877169;
    --text:#f3f0ef;
    --muted:#a0908a;
    --brand:#e1634a;
    --brand2:#e2826f;
    --on-brand:#18110f;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ee6879;
    --violet:#b17af0;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ee6879;
    --header:rgba(27,18,15,.86);
    --overlay:rgba(10,7,5,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#2f221d 8%,#4a352e 22%,#2f221d 36%);
    --s-plan-bg:#401e18;
    --s-plan-fg:#e96449;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#41322d;
    --s-leave-fg:#ab9a94;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(225,99,74,.35);
  }
  [data-palette="clay"][data-theme="light"]{
    color-scheme:light;
    --bg:#f5ede7;
    --panel:#fefefe;
    --panel2:#efe4dc;
    --line:#e3d6cc;
    --line-strong:#977f6d;
    --text:#271f19;
    --muted:#796353;
    --brand:#b75a48;
    --brand2:#a24d3c;
    --on-brand:#ffffff;
    --green:#0a7637;
    --amber:#8b5c0b;
    --red:#cd112a;
    --violet:#7312e2;
    --green-solid:#0f9748;
    --amber-solid:#b07611;
    --red-solid:#ec3c53;
    --header:rgba(245,237,231,.88);
    --overlay:rgba(38,30,23,.45);
    --shadow:0 10px 30px rgba(66,49,36,.13);
    --shadow-sm:0 2px 8px rgba(66,49,36,.09);
    --skel:linear-gradient(90deg,#ece0d7 8%,#f8f6f4 22%,#ece0d7 36%);
    --s-plan-bg:#f2e7e4;
    --s-plan-fg:#b04833;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#eae1d9;
    --s-leave-fg:#775f4e;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(183,90,72,.28);
  }
  [data-palette="harbor"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0d111d;
    --panel:#161c2d;
    --panel2:#21273b;
    --line:#394158;
    --line-strong:#67708e;
    --text:#eff0f3;
    --muted:#878ea3;
    --brand:#6682f2;
    --brand2:#6f89ee;
    --on-brand:#0d1019;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ed5e71;
    --violet:#ad73ef;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ed5e71;
    --header:rgba(13,17,29,.86);
    --overlay:rgba(5,6,11,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1b2032 8%,#2a324e 22%,#1b2032 36%);
    --s-plan-bg:#182040;
    --s-plan-fg:#6782ec;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2a3043;
    --s-leave-fg:#9198ae;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(102,130,242,.35);
  }
  [data-palette="harbor"][data-theme="light"]{
    color-scheme:light;
    --bg:#f4f0e8;
    --panel:#fefefe;
    --panel2:#eee8dd;
    --line:#e1dace;
    --line-strong:#90836c;
    --text:#26221a;
    --muted:#726652;
    --brand:#576fce;
    --brand2:#3e5acb;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#8e5e0b;
    --red:#d0112a;
    --violet:#7312e2;
    --green-solid:#0f9949;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(244,240,232,.88);
    --overlay:rgba(37,32,24,.45);
    --shadow:0 10px 30px rgba(64,54,38,.13);
    --shadow-sm:0 2px 8px rgba(64,54,38,.09);
    --skel:linear-gradient(90deg,#ebe4d9 8%,#f8f7f4 22%,#ebe4d9 36%);
    --s-plan-bg:#e2e6f4;
    --s-plan-fg:#314cba;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e9e4da;
    --s-leave-fg:#71644e;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(87,111,206,.28);
  }
  [data-palette="olive"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#151a0f;
    --panel:#22291a;
    --panel2:#2e3725;
    --line:#49533e;
    --line-strong:#758167;
    --text:#f1f2f0;
    --muted:#969f8c;
    --brand:#add256;
    --brand2:#bbd77a;
    --on-brand:#13170f;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#f07888;
    --violet:#b885f1;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#f07888;
    --header:rgba(21,26,15,.86);
    --overlay:rgba(8,10,6,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#272e1e 8%,#3d482f 22%,#272e1e 36%);
    --s-plan-bg:#344018;
    --s-plan-fg:#b5e740;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#373f2e;
    --s-leave-fg:#a1aa97;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(173,210,86,.35);
  }
  [data-palette="olive"][data-theme="light"]{
    color-scheme:light;
    --bg:#f5f2e8;
    --panel:#fefefe;
    --panel2:#eeeadd;
    --line:#e2ddcd;
    --line-strong:#8e8669;
    --text:#272319;
    --muted:#70684f;
    --brand:#6b7d3f;
    --brand2:#5e6f36;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#905f0c;
    --red:#d2112b;
    --violet:#7312e2;
    --green-solid:#0f9b4a;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(245,242,232,.88);
    --overlay:rgba(38,34,24,.45);
    --shadow:0 10px 30px rgba(65,58,37,.13);
    --shadow-sm:0 2px 8px rgba(65,58,37,.09);
    --skel:linear-gradient(90deg,#ebe7d8 8%,#f8f7f4 22%,#ebe7d8 36%);
    --s-plan-bg:#eef2e4;
    --s-plan-fg:#5c7522;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e9e6da;
    --s-leave-fg:#6f674b;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(107,125,63,.28);
  }
  [data-palette="teal"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0e191c;
    --panel:#18272b;
    --panel2:#233439;
    --line:#3c5055;
    --line-strong:#667f85;
    --text:#eff2f3;
    --muted:#899ca1;
    --brand:#4cd2dc;
    --brand2:#72d7df;
    --on-brand:#0e1618;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ef7182;
    --violet:#b581f1;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ef7182;
    --header:rgba(14,25,28,.86);
    --overlay:rgba(5,9,10,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1c2c30 8%,#2d454b 22%,#1c2c30 36%);
    --s-plan-bg:#183d40;
    --s-plan-fg:#40dce7;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2c3d41;
    --s-leave-fg:#93a7ac;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(76,210,220,.35);
  }
  [data-palette="teal"][data-theme="light"]{
    color-scheme:light;
    --bg:#f2eeea;
    --panel:#fefefe;
    --panel2:#ebe5e0;
    --line:#ded7d1;
    --line-strong:#8e8176;
    --text:#24201c;
    --muted:#71655b;
    --brand:#398186;
    --brand2:#2f7074;
    --on-brand:#ffffff;
    --green:#0a7637;
    --amber:#8b5c0b;
    --red:#cd112a;
    --violet:#7312e2;
    --green-solid:#0f9748;
    --amber-solid:#b07611;
    --red-solid:#ec3c53;
    --header:rgba(242,238,234,.88);
    --overlay:rgba(35,30,26,.45);
    --shadow:0 10px 30px rgba(60,50,42,.13);
    --shadow-sm:0 2px 8px rgba(60,50,42,.09);
    --skel:linear-gradient(90deg,#e8e1dc 8%,#f7f6f5 22%,#e8e1dc 36%);
    --s-plan-bg:#e4f1f2;
    --s-plan-fg:#24777d;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e7e1dd;
    --s-leave-fg:#6f6358;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(57,129,134,.28);
  }
  [data-palette="sky"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0f141b;
    --panel:#19202b;
    --panel2:#242c38;
    --line:#3d4755;
    --line-strong:#697587;
    --text:#eff1f3;
    --muted:#8a93a0;
    --brand:#3087ff;
    --brand2:#529aff;
    --on-brand:#0f1218;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ee6577;
    --violet:#b078f0;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ee6577;
    --header:rgba(15,20,27,.86);
    --overlay:rgba(5,7,10,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1d252f 8%,#2e3a4a 22%,#1d252f 36%);
    --s-plan-bg:#182840;
    --s-plan-fg:#4e8fe9;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2d3541;
    --s-leave-fg:#949dab;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(48,135,255,.35);
  }
  [data-palette="sky"][data-theme="light"]{
    color-scheme:light;
    --bg:#e9eef4;
    --panel:#fefefe;
    --panel2:#dee5ed;
    --line:#ced7e1;
    --line-strong:#748395;
    --text:#1a1f26;
    --muted:#596779;
    --brand:#2373e4;
    --brand2:#1461cc;
    --on-brand:#ffffff;
    --green:#0a7637;
    --amber:#8b5c0b;
    --red:#cb1029;
    --violet:#7312e2;
    --green-solid:#0f9546;
    --amber-solid:#b07611;
    --red-solid:#ec3c53;
    --header:rgba(233,238,244,.88);
    --overlay:rgba(24,30,37,.45);
    --shadow:0 10px 30px rgba(39,50,63,.13);
    --shadow-sm:0 2px 8px rgba(39,50,63,.09);
    --skel:linear-gradient(90deg,#d9e1ea 8%,#f4f6f8 22%,#d9e1ea 36%);
    --s-plan-bg:#dfe9f7;
    --s-plan-fg:#1b65cd;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#dbe1e9;
    --s-leave-fg:#556577;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(35,115,228,.28);
  }
  [data-palette="mint"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0f1a19;
    --panel:#1a2927;
    --panel2:#253734;
    --line:#3e5350;
    --line-strong:#68827e;
    --text:#f0f2f2;
    --muted:#8c9f9c;
    --brand:#29ffd4;
    --brand2:#52ffdc;
    --on-brand:#0f1716;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#f07586;
    --violet:#b885f1;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#f07586;
    --header:rgba(15,26,25,.86);
    --overlay:rgba(6,10,9,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#1e2e2c 8%,#2f4844 22%,#1e2e2c 36%);
    --s-plan-bg:#184038;
    --s-plan-fg:#40e7c6;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#2e3f3d;
    --s-leave-fg:#97aaa7;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(41,255,212,.35);
  }
  [data-palette="mint"][data-theme="light"]{
    color-scheme:light;
    --bg:#eaf3f1;
    --panel:#fefefe;
    --panel2:#dfece9;
    --line:#d0dfdc;
    --line-strong:#6f8b84;
    --text:#1b2522;
    --muted:#556d67;
    --brand:#14856f;
    --brand2:#0f7661;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#905f0c;
    --red:#d0112a;
    --violet:#7312e2;
    --green-solid:#0f9949;
    --amber-solid:#b57912;
    --red-solid:#ec3c53;
    --header:rgba(234,243,241,.88);
    --overlay:rgba(25,36,33,.45);
    --shadow:0 10px 30px rgba(41,61,56,.13);
    --shadow-sm:0 2px 8px rgba(41,61,56,.09);
    --skel:linear-gradient(90deg,#dbe9e5 8%,#f5f7f7 22%,#dbe9e5 36%);
    --s-plan-bg:#dff7f2;
    --s-plan-fg:#137c67;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#dce7e5;
    --s-leave-fg:#516c65;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(20,133,111,.28);
  }
  [data-palette="lavender"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#140f1b;
    --panel:#20192a;
    --panel2:#2c2437;
    --line:#463d54;
    --line-strong:#786c89;
    --text:#f1eff3;
    --muted:#938ba0;
    --brand:#a16bff;
    --brand2:#a673ff;
    --on-brand:#120f17;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ed5e71;
    --violet:#ad73ef;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ed5e71;
    --header:rgba(20,15,27,.86);
    --overlay:rgba(7,6,10,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#251e2f 8%,#392f49 22%,#251e2f 36%);
    --s-plan-bg:#261840;
    --s-plan-fg:#9c6eed;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#352e40;
    --s-leave-fg:#9d95aa;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(161,107,255,.35);
  }
  [data-palette="lavender"][data-theme="light"]{
    color-scheme:light;
    --bg:#f2efeb;
    --panel:#fefefe;
    --panel2:#ebe6e0;
    --line:#ded8d1;
    --line-strong:#8c8276;
    --text:#24201c;
    --muted:#6f655b;
    --brand:#7e40e7;
    --brand2:#6a20e9;
    --on-brand:#ffffff;
    --green:#0a7838;
    --amber:#8e5e0b;
    --red:#cd112a;
    --violet:#7312e2;
    --green-solid:#0f9748;
    --amber-solid:#b37812;
    --red-solid:#ec3c53;
    --header:rgba(242,239,235,.88);
    --overlay:rgba(35,31,27,.45);
    --shadow:0 10px 30px rgba(59,52,43,.13);
    --shadow-sm:0 2px 8px rgba(59,52,43,.09);
    --skel:linear-gradient(90deg,#e7e2dc 8%,#f7f6f5 22%,#e7e2dc 36%);
    --s-plan-bg:#e8dff7;
    --s-plan-fg:#5d1bd0;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e6e2dd;
    --s-leave-fg:#6d6357;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(126,64,231,.28);
  }
  [data-palette="rose"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1b0f13;
    --panel:#2a191f;
    --panel2:#37242b;
    --line:#543d46;
    --line-strong:#876b75;
    --text:#f3eff1;
    --muted:#a08b92;
    --brand:#ee5078;
    --brand2:#eb6587;
    --on-brand:#170f12;
    --green:#30e87d;
    --amber:#e8a530;
    --red:#ed6173;
    --violet:#ae75f0;
    --green-solid:#30e87d;
    --amber-solid:#e8a530;
    --red-solid:#ed6173;
    --header:rgba(27,15,19,.86);
    --overlay:rgba(10,6,7,.72);
    --shadow:0 10px 30px rgba(0,0,0,.45);
    --shadow-sm:0 2px 8px rgba(0,0,0,.30);
    --skel:linear-gradient(90deg,#2f1e24 8%,#492f38 22%,#2f1e24 36%);
    --s-plan-bg:#401822;
    --s-plan-fg:#eb597e;
    --s-prog-bg:#3e3119;
    --s-prog-fg:#e7aa40;
    --s-done-bg:#193e29;
    --s-done-fg:#40e786;
    --s-carry-bg:#3e191e;
    --s-carry-fg:#eb596d;
    --s-leave-bg:#402e34;
    --s-leave-fg:#aa959d;
    --hol-bg:rgba(240,76,98,.10);
    --hol-bd:rgba(240,76,98,.45);
    --hol-chip:rgba(240,76,98,.22);
    --hol-fg:#f87788;
    --warn-bg:#3a2c15;
    --warn-bd:#5d4928;
    --warn-fg:#eea62b;
    --ring:rgba(238,80,120,.35);
  }
  [data-palette="rose"][data-theme="light"]{
    color-scheme:light;
    --bg:#f4ede9;
    --panel:#fefefe;
    --panel2:#ede4de;
    --line:#e0d6cf;
    --line-strong:#938073;
    --text:#251f1a;
    --muted:#756357;
    --brand:#c74a69;
    --brand2:#b93456;
    --on-brand:#ffffff;
    --green:#0a7637;
    --amber:#8b5c0b;
    --red:#cd112a;
    --violet:#7312e2;
    --green-solid:#0f9748;
    --amber-solid:#b07611;
    --red-solid:#ec3c53;
    --header:rgba(244,237,233,.88);
    --overlay:rgba(36,29,25,.45);
    --shadow:0 10px 30px rgba(62,49,40,.13);
    --shadow-sm:0 2px 8px rgba(62,49,40,.09);
    --skel:linear-gradient(90deg,#eae0da 8%,#f8f6f5 22%,#eae0da 36%);
    --s-plan-bg:#f3e3e7;
    --s-plan-fg:#b83254;
    --s-prog-bg:#f9efdd;
    --s-prog-fg:#95630c;
    --s-done-bg:#ddf9e9;
    --s-done-fg:#0a7f3b;
    --s-carry-bg:#f9dde1;
    --s-carry-fg:#cb1029;
    --s-leave-bg:#e8e0db;
    --s-leave-fg:#736054;
    --hol-bg:rgba(226,54,77,.07);
    --hol-bd:rgba(226,54,77,.32);
    --hol-chip:rgba(226,54,77,.13);
    --hol-fg:#c1152c;
    --warn-bg:#fef4e4;
    --warn-bd:#efdbb9;
    --warn-fg:#9a6406;
    --ring:rgba(199,74,105,.28);
  }

  [data-palette="peach"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#20130d;--panel:#2b1a12;--panel2:#39251b;--line:#4a3a30;--line-strong:#7d6a5a;
    --text:#f3efec;--muted:#a08e82;--brand:#ff8f6b;--brand2:#ffab8e;--on-brand:#1a0f0a;
    --header:rgba(32,19,13,.86);--overlay:rgba(10,6,4,.72);
    --skel:linear-gradient(90deg,#2e1d14 8%,#463025 22%,#2e1d14 36%);
    --s-plan-bg:#3f2416;--s-plan-fg:#ff916c;--s-leave-bg:#37251c;--s-leave-fg:#a8948a;
    --ring:rgba(255,143,107,.35);
  }
  [data-palette="peach"][data-theme="light"]{
    color-scheme:light;
    --bg:#fbf1ea;--panel:#fffdfb;--panel2:#f3e3d8;--line:#ead3c4;--line-strong:#a0775f;
    --text:#2b1d15;--muted:#7d5f4b;--brand:#c96a45;--brand2:#b35634;--on-brand:#ffffff;
    --header:rgba(251,241,234,.88);--overlay:rgba(39,26,17,.45);
    --skel:linear-gradient(90deg,#f0ddd0 8%,#fdf9f6 22%,#f0ddd0 36%);
    --s-plan-bg:#f6e6dc;--s-plan-fg:#bd5f3b;--s-leave-bg:#eee2da;--s-leave-fg:#77594a;
    --ring:rgba(201,106,69,.28);
  }
  [data-palette="blossom"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1e1218;--panel:#291a22;--panel2:#372430;--line:#4d3a44;--line-strong:#7f6470;
    --text:#f4eef1;--muted:#a08b94;--brand:#f27fb0;--brand2:#f69ac2;--on-brand:#180d12;
    --header:rgba(30,18,24,.86);--overlay:rgba(8,5,7,.72);
    --skel:linear-gradient(90deg,#2c1c24 8%,#44303a 22%,#2c1c24 36%);
    --s-plan-bg:#3a2030;--s-plan-fg:#f57fb4;--s-leave-bg:#34252e;--s-leave-fg:#a3909a;
    --ring:rgba(242,127,176,.35);
  }
  [data-palette="blossom"][data-theme="light"]{
    color-scheme:light;
    --bg:#faf0f4;--panel:#fffdfe;--panel2:#f4e2ea;--line:#eccfdc;--line-strong:#a06a83;
    --text:#2c1d24;--muted:#82606f;--brand:#cf4d88;--brand2:#b23a72;--on-brand:#ffffff;
    --header:rgba(250,240,244,.88);--overlay:rgba(42,24,32,.45);
    --skel:linear-gradient(90deg,#f1dde7 8%,#fdf8fa 22%,#f1dde7 36%);
    --s-plan-bg:#f5e3e7;--s-plan-fg:#bd4578;--s-leave-bg:#eee0e6;--s-leave-fg:#7c6270;
    --ring:rgba(207,77,136,.28);
  }
  [data-palette="berry"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1c0f15;--panel:#271620;--panel2:#352030;--line:#4f3a46;--line-strong:#816471;
    --text:#f3edf0;--muted:#a08793;--brand:#ff5d8f;--brand2:#ff7fa7;--on-brand:#160b10;
    --header:rgba(28,15,21,.86);--overlay:rgba(9,5,7,.72);
    --skel:linear-gradient(90deg,#2b1821 8%,#44303c 22%,#2b1821 36%);
    --s-plan-bg:#3a1e2c;--s-plan-fg:#ff6194;--s-leave-bg:#33222c;--s-leave-fg:#a3919a;
    --ring:rgba(255,93,143,.35);
  }
  [data-palette="berry"][data-theme="light"]{
    color-scheme:light;
    --bg:#fbf0f3;--panel:#fffdfe;--panel2:#f5e1e8;--line:#efccd6;--line-strong:#a2687b;
    --text:#2d1c22;--muted:#83606c;--brand:#d23b68;--brand2:#b52b55;--on-brand:#ffffff;
    --header:rgba(251,240,243,.88);--overlay:rgba(43,23,29,.45);
    --skel:linear-gradient(90deg,#f2dde5 8%,#fdf8fa 22%,#f2dde5 36%);
    --s-plan-bg:#f5dfe7;--s-plan-fg:#c0345e;--s-leave-bg:#efe0e4;--s-leave-fg:#7e616c;
    --ring:rgba(210,59,104,.28);
  }
  [data-palette="plum"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#180f1d;--panel:#221631;--panel2:#2f2140;--line:#4a3b57;--line-strong:#7b688f;
    --text:#f1eef4;--muted:#9d8baa;--brand:#b57edc;--brand2:#c69ae6;--on-brand:#120a17;
    --header:rgba(24,15,29,.86);--overlay:rgba(8,5,10,.72);
    --skel:linear-gradient(90deg,#261a33 8%,#3b2b4c 22%,#261a33 36%);
    --s-plan-bg:#33204a;--s-plan-fg:#b984e0;--s-leave-bg:#2e2338;--s-leave-fg:#a391b0;
    --ring:rgba(181,126,220,.35);
  }
  [data-palette="plum"][data-theme="light"]{
    color-scheme:light;
    --bg:#f5f0f8;--panel:#fefdff;--panel2:#ece1f3;--line:#e2d3ea;--line-strong:#8f6fa4;
    --text:#281f2d;--muted:#735f80;--brand:#7d3f9e;--brand2:#6a2f8a;--on-brand:#ffffff;
    --header:rgba(245,240,248,.88);--overlay:rgba(37,28,43,.45);
    --skel:linear-gradient(90deg,#e9dcf1 8%,#fbf9fd 22%,#e9dcf1 36%);
    --s-plan-bg:#ecddf4;--s-plan-fg:#70378f;--s-leave-bg:#e9e1ee;--s-leave-fg:#6f5f7d;
    --ring:rgba(125,63,158,.28);
  }
  [data-palette="orchid"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#150f1c;--panel:#1f1729;--panel2:#2b2138;--line:#463b54;--line-strong:#75668d;
    --text:#f2eef5;--muted:#9a8ca6;--brand:#c792e8;--brand2:#d8abf0;--on-brand:#120b17;
    --header:rgba(21,15,28,.86);--overlay:rgba(7,5,10,.72);
    --skel:linear-gradient(90deg,#231a30 8%,#372a49 22%,#231a30 36%);
    --s-plan-bg:#2f1e45;--s-plan-fg:#c995e9;--s-leave-bg:#2c2436;--s-leave-fg:#a092ad;
    --ring:rgba(199,146,232,.35);
  }
  [data-palette="orchid"][data-theme="light"]{
    color-scheme:light;
    --bg:#f3eef7;--panel:#fefdff;--panel2:#e9e0f2;--line:#dfd2e9;--line-strong:#8a6aa4;
    --text:#281f2d;--muted:#705b80;--brand:#8441a8;--brand2:#712f92;--on-brand:#ffffff;
    --header:rgba(243,238,247,.88);--overlay:rgba(36,27,43,.45);
    --skel:linear-gradient(90deg,#e6daf0 8%,#fbf9fd 22%,#e6daf0 36%);
    --s-plan-bg:#e8daf2;--s-plan-fg:#773a9c;--s-leave-bg:#e8e0ee;--s-leave-fg:#6e5d7e;
    --ring:rgba(132,65,168,.28);
  }
  [data-palette="wine"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1a0f10;--panel:#251618;--panel2:#332024;--line:#503a3e;--line-strong:#836a6f;
    --text:#f2eded;--muted:#a08b8f;--brand:#e2647d;--brand2:#ea8297;--on-brand:#140a0c;
    --header:rgba(26,15,16,.86);--overlay:rgba(9,5,6,.72);
    --skel:linear-gradient(90deg,#291a1c 8%,#403037 22%,#291a1c 36%);
    --s-plan-bg:#3c1e28;--s-plan-fg:#e66b83;--s-leave-bg:#332327;--s-leave-fg:#a28f93;
    --ring:rgba(226,100,125,.35);
  }
  [data-palette="wine"][data-theme="light"]{
    color-scheme:light;
    --bg:#faf0f1;--panel:#fffdfd;--panel2:#f4e2e4;--line:#ecd0d4;--line-strong:#a06872;
    --text:#2d1d20;--muted:#82606a;--brand:#c03a55;--brand2:#a42b45;--on-brand:#ffffff;
    --header:rgba(250,240,241,.88);--overlay:rgba(43,24,27,.45);
    --skel:linear-gradient(90deg,#f1dde1 8%,#fdf8f9 22%,#f1dde1 36%);
    --s-plan-bg:#f5e0e4;--s-plan-fg:#b3344f;--s-leave-bg:#efe1e3;--s-leave-fg:#7e636a;
    --ring:rgba(192,58,85,.28);
  }
  [data-palette="coral"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#1c110e;--panel:#271815;--panel2:#352420;--line:#4f3d38;--line-strong:#826c65;
    --text:#f3efed;--muted:#a18f88;--brand:#ff8066;--brand2:#ff9a84;--on-brand:#160d0a;
    --header:rgba(28,17,14,.86);--overlay:rgba(10,6,5,.72);
    --skel:linear-gradient(90deg,#2b1a16 8%,#44302a 22%,#2b1a16 36%);
    --s-plan-bg:#3c2418;--s-plan-fg:#ff8468;--s-leave-bg:#352720;--s-leave-fg:#a5948d;
    --ring:rgba(255,128,102,.35);
  }
  [data-palette="coral"][data-theme="light"]{
    color-scheme:light;
    --bg:#fbf1ee;--panel:#fffdfc;--panel2:#f5e3de;--line:#edcec7;--line-strong:#a3715f;
    --text:#2d1e19;--muted:#84634f;--brand:#d05a3e;--brand2:#b5482e;--on-brand:#ffffff;
    --header:rgba(251,241,238,.88);--overlay:rgba(43,26,20,.45);
    --skel:linear-gradient(90deg,#f2dfd8 8%,#fdf9f7 22%,#f2dfd8 36%);
    --s-plan-bg:#f6e2da;--s-plan-fg:#c35337;--s-leave-bg:#f0e3dd;--s-leave-fg:#7f6557;
    --ring:rgba(208,90,62,.28);
  }
  [data-palette="gold"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#181408;--panel:#231d0e;--panel2:#302817;--line:#4c422c;--line-strong:#7f7150;
    --text:#f3f0e9;--muted:#a2957a;--brand:#e8b64a;--brand2:#f0c76e;--on-brand:#151006;
    --header:rgba(24,20,8,.86);--overlay:rgba(8,7,3,.72);
    --skel:linear-gradient(90deg,#27200f 8%,#3e3520 22%,#27200f 36%);
    --s-plan-bg:#3c3112;--s-plan-fg:#eab84e;--s-leave-bg:#332b1c;--s-leave-fg:#a5977d;
    --ring:rgba(232,182,74,.35);
  }
  [data-palette="gold"][data-theme="light"]{
    color-scheme:light;
    --bg:#faf5e8;--panel:#fffdf6;--panel2:#f3ead2;--line:#e8dbb8;--line-strong:#9c844c;
    --text:#2d2513;--muted:#7c6a3d;--brand:#b8871a;--brand2:#9c710f;--on-brand:#ffffff;
    --header:rgba(250,245,232,.88);--overlay:rgba(43,35,15,.45);
    --skel:linear-gradient(90deg,#f0e6cb 8%,#fdfbf4 22%,#f0e6cb 36%);
    --s-plan-bg:#f2e6c6;--s-plan-fg:#a87815;--s-leave-bg:#ede5d3;--s-leave-fg:#79683f;
    --ring:rgba(184,135,26,.28);
  }
  [data-palette="ocean"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0b1620;--panel:#13212e;--panel2:#1d2f3f;--line:#33495c;--line-strong:#647f93;
    --text:#edf1f4;--muted:#8498a6;--brand:#4fc3f7;--brand2:#72d0f9;--on-brand:#0a141c;
    --header:rgba(11,22,32,.86);--overlay:rgba(4,9,13,.72);
    --skel:linear-gradient(90deg,#172735 8%,#2a4052 22%,#172735 36%);
    --s-plan-bg:#10283c;--s-plan-fg:#53c6f8;--s-leave-bg:#1e2b33;--s-leave-fg:#8fa3b1;
    --ring:rgba(79,195,247,.35);
  }
  [data-palette="ocean"][data-theme="light"]{
    color-scheme:light;
    --bg:#ecf4f8;--panel:#fdfefe;--panel2:#dde9f0;--line:#ccdde7;--line-strong:#6d8a9d;
    --text:#1d262c;--muted:#5f7483;--brand:#1279a3;--brand2:#0d6489;--on-brand:#ffffff;
    --header:rgba(236,244,248,.88);--overlay:rgba(26,37,44,.45);
    --skel:linear-gradient(90deg,#dbe8ef 8%,#fafdfe 22%,#dbe8ef 36%);
    --s-plan-bg:#dcecf4;--s-plan-fg:#116f96;--s-leave-bg:#e2e9ec;--s-leave-fg:#5f7382;
    --ring:rgba(18,121,163,.28);
  }
  [data-palette="forest"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#0c1710;--panel:#132219;--panel2:#1c3023;--line:#334c3e;--line-strong:#64806f;
    --text:#ecf1ee;--muted:#81968b;--brand:#4cbf82;--brand2:#6bcd99;--on-brand:#0a1410;
    --header:rgba(12,23,16,.86);--overlay:rgba(4,9,6,.72);
    --skel:linear-gradient(90deg,#16271d 8%,#28402f 22%,#16271d 36%);
    --s-plan-bg:#143321;--s-plan-fg:#52c387;--s-leave-bg:#1e2c24;--s-leave-fg:#8ca094;
    --ring:rgba(76,191,130,.35);
  }
  [data-palette="forest"][data-theme="light"]{
    color-scheme:light;
    --bg:#ebf4ee;--panel:#fdfefd;--panel2:#dcebe2;--line:#cbdfd2;--line-strong:#698d78;
    --text:#1d2721;--muted:#56705f;--brand:#1a7a47;--brand2:#11663a;--on-brand:#ffffff;
    --header:rgba(235,244,238,.88);--overlay:rgba(24,38,29,.45);
    --skel:linear-gradient(90deg,#d9e8de 8%,#f9fcfb 22%,#d9e8de 36%);
    --s-plan-bg:#dbeee3;--s-plan-fg:#177343;--s-leave-bg:#e2ebe5;--s-leave-fg:#5f7669;
    --ring:rgba(26,122,71,.28);
  }
  [data-palette="slate"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#101419;--panel:#181d24;--panel2:#222830;--line:#3a424d;--line-strong:#6a7684;
    --text:#eef1f3;--muted:#8b96a1;--brand:#93aec9;--brand2:#a9c0d8;--on-brand:#0f1419;
    --header:rgba(16,20,25,.86);--overlay:rgba(5,7,9,.72);
    --skel:linear-gradient(90deg,#1b2129 8%,#2e3741 22%,#1b2129 36%);
    --s-plan-bg:#1c2a38;--s-plan-fg:#97b2cd;--s-leave-bg:#232a31;--s-leave-fg:#909ba5;
    --ring:rgba(147,174,201,.35);
  }
  [data-palette="slate"][data-theme="light"]{
    color-scheme:light;
    --bg:#eef1f4;--panel:#fdfefe;--panel2:#e0e6eb;--line:#d0d8df;--line-strong:#74818f;
    --text:#22272c;--muted:#5f6b76;--brand:#5c6f85;--brand2:#4b5c70;--on-brand:#ffffff;
    --header:rgba(238,241,244,.88);--overlay:rgba(30,35,40,.45);
    --skel:linear-gradient(90deg,#dde4ea 8%,#fafcfd 22%,#dde4ea 36%);
    --s-plan-bg:#dfe7ee;--s-plan-fg:#54667b;--s-leave-bg:#e3e8ec;--s-leave-fg:#5d6975;
    --ring:rgba(92,111,133,.28);
  }
  [data-palette="dusk"][data-theme="dark"]{
    color-scheme:dark;
    --bg:#130f1c;--panel:#1c1729;--panel2:#282138;--line:#413852;--line-strong:#6f6290;
    --text:#f0edf4;--muted:#978ba6;--brand:#8f7ae5;--brand2:#a695eb;--on-brand:#100c18;
    --header:rgba(19,15,28,.86);--overlay:rgba(7,5,11,.72);
    --skel:linear-gradient(90deg,#201a30 8%,#332a4a 22%,#201a30 36%);
    --s-plan-bg:#251b3f;--s-plan-fg:#9482e9;--s-leave-bg:#2c2438;--s-leave-fg:#9b8fae;
    --ring:rgba(143,122,229,.35);
  }
  [data-palette="dusk"][data-theme="light"]{
    color-scheme:light;
    --bg:#f1eef8;--panel:#fdfdff;--panel2:#e6e0f4;--line:#dbd3ec;--line-strong:#8477a8;
    --text:#26212e;--muted:#6d6382;--brand:#5f4ab8;--brand2:#4d3aa0;--on-brand:#ffffff;
    --header:rgba(241,238,248,.88);--overlay:rgba(34,28,44,.45);
    --skel:linear-gradient(90deg,#e3dbf2 8%,#fbfafe 22%,#e3dbf2 36%);
    --s-plan-bg:#e5dcf4;--s-plan-fg:#5742a9;--s-leave-bg:#e8e3ef;--s-leave-fg:#6a607d;
    --ring:rgba(95,74,184,.28);
  }

  /* ===== THEME-SWITCH TRANSITIONS =====
     Register every colour/shadow/percentage token the palette and theme blocks
     override, so the custom properties themselves interpolate when data-palette /
     data-theme change. Every surface, glass tint (color-mix) and ambient orb
     (background-image) then cross-fades automatically. Browsers without
     @property just snap — same as before. */
  @property --bg{syntax:'<color>';inherits:true;initial-value:#0e101c;}
  @property --panel{syntax:'<color>';inherits:true;initial-value:#181a2c;}
  @property --panel2{syntax:'<color>';inherits:true;initial-value:#222539;}
  @property --line{syntax:'<color>';inherits:true;initial-value:#3b3f56;}
  @property --line-strong{syntax:'<color>';inherits:true;initial-value:#696e8c;}
  @property --text{syntax:'<color>';inherits:true;initial-value:#eff0f3;}
  @property --muted{syntax:'<color>';inherits:true;initial-value:#898ca2;}
  @property --brand{syntax:'<color>';inherits:true;initial-value:#e66842;}
  @property --brand2{syntax:'<color>';inherits:true;initial-value:#e7876a;}
  @property --on-brand{syntax:'<color>';inherits:true;initial-value:#0e0f18;}
  @property --green{syntax:'<color>';inherits:true;initial-value:#30e87d;}
  @property --amber{syntax:'<color>';inherits:true;initial-value:#e8a530;}
  @property --red{syntax:'<color>';inherits:true;initial-value:#ed5a6d;}
  @property --violet{syntax:'<color>';inherits:true;initial-value:#ac71ef;}
  @property --green-solid{syntax:'<color>';inherits:true;initial-value:#30e87d;}
  @property --amber-solid{syntax:'<color>';inherits:true;initial-value:#e8a530;}
  @property --red-solid{syntax:'<color>';inherits:true;initial-value:#ed5a6d;}
  @property --header{syntax:'<color>';inherits:true;initial-value:rgba(14,16,28,.86);}
  @property --overlay{syntax:'<color>';inherits:true;initial-value:rgba(5,6,10,.72);}
  @property --s-plan-bg{syntax:'<color>';inherits:true;initial-value:#402118;}
  @property --s-plan-fg{syntax:'<color>';inherits:true;initial-value:#e86943;}
  @property --s-prog-bg{syntax:'<color>';inherits:true;initial-value:#3e3119;}
  @property --s-prog-fg{syntax:'<color>';inherits:true;initial-value:#e7aa40;}
  @property --s-done-bg{syntax:'<color>';inherits:true;initial-value:#193e29;}
  @property --s-done-fg{syntax:'<color>';inherits:true;initial-value:#40e786;}
  @property --s-carry-bg{syntax:'<color>';inherits:true;initial-value:#3e191e;}
  @property --s-carry-fg{syntax:'<color>';inherits:true;initial-value:#eb596d;}
  @property --s-leave-bg{syntax:'<color>';inherits:true;initial-value:#2c2f42;}
  @property --s-leave-fg{syntax:'<color>';inherits:true;initial-value:#9296ac;}
  @property --hol-bg{syntax:'<color>';inherits:true;initial-value:rgba(240,76,98,.10);}
  @property --hol-bd{syntax:'<color>';inherits:true;initial-value:rgba(240,76,98,.45);}
  @property --hol-chip{syntax:'<color>';inherits:true;initial-value:rgba(240,76,98,.22);}
  @property --hol-fg{syntax:'<color>';inherits:true;initial-value:#f87788;}
  @property --warn-bg{syntax:'<color>';inherits:true;initial-value:#3a2c15;}
  @property --warn-bd{syntax:'<color>';inherits:true;initial-value:#5d4928;}
  @property --warn-fg{syntax:'<color>';inherits:true;initial-value:#eea62b;}
  @property --ring{syntax:'<color>';inherits:true;initial-value:rgba(230,104,66,.35);}
  @property --glass-hi{syntax:'<color>';inherits:true;initial-value:rgba(255,255,255,.10);}
  @property --glass-lo{syntax:'<color>';inherits:true;initial-value:rgba(0,0,0,.30);}
  @property --shadow{syntax:'<shadow>';inherits:true;initial-value:0 10px 30px rgba(0,0,0,.45);}
  @property --shadow-sm{syntax:'<shadow>';inherits:true;initial-value:0 2px 8px rgba(0,0,0,.30);}
  @property --glass-drop{syntax:'<shadow>#';inherits:true;initial-value:0 0 0 transparent;}
  @property --glass-a{syntax:'<percentage>';inherits:true;initial-value:55%;}
  @property --glass-a2{syntax:'<percentage>';inherits:true;initial-value:62%;}
  @property --glass-a3{syntax:'<percentage>';inherits:true;initial-value:97%;}
  @property --orb-a{syntax:'<percentage>';inherits:true;initial-value:15%;}
  @property --orb-b{syntax:'<percentage>';inherits:true;initial-value:12%;}
  @property --orb-c{syntax:'<percentage>';inherits:true;initial-value:9%;}

  /* Shared scales — spacing on a 4/8 rhythm at dashboard density, one icon size
     token per role so strokes and alignment stay consistent. */
  :root{
    --sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:16px;--sp-5:24px;--sp-6:32px;
    --icon-sm:16px;--icon-md:20px;--icon-lg:24px;
    --tap:32px;                /* pointer-precise minimum (WCAG 2.2 AA needs >=24) */
    --dur-fast:150ms;--dur:220ms;--dur-slow:320ms;--ease:cubic-bezier(.2,.8,.3,1);
    /* theme/palette switches interpolate the registered tokens above, so every
       surface, glass tint, shadow and ambient orb cross-fades (no @property
       support = instant, as before). prefers-reduced-motion kills this too. */
    transition:--bg 280ms var(--ease),--panel 280ms var(--ease),--panel2 280ms var(--ease),--line 280ms var(--ease),--line-strong 280ms var(--ease),--text 280ms var(--ease),--muted 280ms var(--ease),--brand 280ms var(--ease),--brand2 280ms var(--ease),--on-brand 280ms var(--ease),--green 280ms var(--ease),--amber 280ms var(--ease),--red 280ms var(--ease),--violet 280ms var(--ease),--green-solid 280ms var(--ease),--amber-solid 280ms var(--ease),--red-solid 280ms var(--ease),--header 280ms var(--ease),--overlay 280ms var(--ease),--s-plan-bg 280ms var(--ease),--s-plan-fg 280ms var(--ease),--s-prog-bg 280ms var(--ease),--s-prog-fg 280ms var(--ease),--s-done-bg 280ms var(--ease),--s-done-fg 280ms var(--ease),--s-carry-bg 280ms var(--ease),--s-carry-fg 280ms var(--ease),--s-leave-bg 280ms var(--ease),--s-leave-fg 280ms var(--ease),--hol-bg 280ms var(--ease),--hol-bd 280ms var(--ease),--hol-chip 280ms var(--ease),--hol-fg 280ms var(--ease),--warn-bg 280ms var(--ease),--warn-bd 280ms var(--ease),--warn-fg 280ms var(--ease),--ring 280ms var(--ease),--glass-hi 280ms var(--ease),--glass-lo 280ms var(--ease),--shadow 280ms var(--ease),--shadow-sm 280ms var(--ease),--glass-drop 280ms var(--ease),--glass-a 280ms var(--ease),--glass-a2 280ms var(--ease),--glass-a3 280ms var(--ease),--orb-a 280ms var(--ease),--orb-b 280ms var(--ease),--orb-c 280ms var(--ease);
  }
  @media (pointer:coarse){ :root{ --tap:44px; } }   /* touch: iOS 44 / Android 48 */

  /* ============ BASE ============ */
  *{box-sizing:border-box;margin:0;padding:0}
  html{-webkit-text-size-adjust:100%}
  /* Base 16px — Thai script in particular is hard to read below it. */
  /* the canvas base colour lives on html so it always fills the full scroll
     extent — even when the content is shorter than the viewport, the area below
     it stays the same --bg instead of exposing the browser canvas. */
  html{background-color:var(--bg)}
  body{font-family:"Sarabun","Noto Sans Thai","Segoe UI",Tahoma,sans-serif;
       background-color:var(--bg);color:var(--text);font-size:16px;line-height:1.5;
       transition:background-color var(--dur) ease,color var(--dur) ease}
  ::selection{background:var(--ring)}
  /* =================== THE APP'S OWN SCROLLBAR ===================
     The browser's default bar is the one piece of chrome that ignored the
     theme: a light grey slab down the side of a dark page, and on Windows a
     16px one that never matched the 22 palettes either side of it.
     Every palette is built from the same tokens, so ONE rule follows all of
     them — light, dark and each accent — with no per-theme copy to keep in
     step. The thumb is painted into its content box behind a transparent 3px
     border: what the pointer can grab is the full 12px, what the eye sees is a
     rounded 6px, which is the only way to get padding around a scrollbar
     thumb in CSS.
     Firefox has no thumb geometry to style — scrollbar-width/color is the
     whole API there, and 'thin' with the same colour is the closest match. The
     two are not in competition: where ::-webkit-scrollbar is understood it
     wins, and where it is not the standard properties are all there is.
     Components that want a slimmer bar still say so (.pop, .bell-body, the
     command palette) and keep winning on specificity. */
  *{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--line-strong) 62%,transparent) transparent}
  ::-webkit-scrollbar{width:12px;height:12px;background:transparent}
  ::-webkit-scrollbar-track{background:transparent}
  ::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--line-strong) 62%,transparent);
    background-clip:content-box;border:3px solid transparent;border-radius:99px}
  ::-webkit-scrollbar-thumb:hover{background:var(--line-strong);background-clip:content-box}
  /* dragging is the one moment the bar is the thing being used, so it takes
     the accent colour — the same feedback every other control here gives */
  ::-webkit-scrollbar-thumb:active{background:var(--brand);background-clip:content-box}
  ::-webkit-scrollbar-corner{background:transparent}
  /* NO RESERVED GUTTER.
     There was a scrollbar-gutter:stable here, to stop the layout jumping by a
     scrollbar's width the moment content grew past the viewport. It cost more
     than it saved: the gutter is reserved whether or not there is anything to
     scroll, and inside the Apps Script frame that reserve was painted with the
     platform's default track — a white band down the right-hand edge of a dark
     app, on every screen short enough not to scroll, with no scrollbar in it to
     explain itself. Reported twice, in those words: "what is the white bar on
     the right", "it is there even when there is no scrollbar".
     Painting it was tried first (html::-webkit-scrollbar-track, and the
     standard scrollbar-color, both to --bg). It works in a plain tab and does
     nothing inside the deployment's iframe, where the reserve is drawn by the
     platform rather than by this page. Nothing reserved is the one state no
     engine can paint wrong.
     The scrollbar itself is still themed below — that part does apply wherever
     the page's own bar is drawn. */
  /* The page's own bar, where the browser lets a page style it at all. */
  html::-webkit-scrollbar,
  html::-webkit-scrollbar-track,
  html::-webkit-scrollbar-corner{background:var(--bg)}
  html{scrollbar-color:color-mix(in srgb,var(--line-strong) 62%,transparent) var(--bg)}
  :focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:6px}
  /* visually-hidden: stays in the accessibility tree (unlike aria-hidden/display:none),
     used to give canvas-based charts a real text alternative for screen readers */
  .vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
      clip:rect(0,0,0,0);white-space:nowrap;border:0}
  .wrap{max-width:1180px;margin:0 auto;padding:0 16px 60px;position:relative;z-index:1}

  /* ---- OFFLINE BANNER ---- */
  /* ---- the quiet nudge (see NUDGES in the script) ----
     A bubble under a header button, with a caret pointing back up at it. Kept
     small, low-contrast and short-lived on purpose: it is a suggestion, and
     anything that reads as a demand here would be worse than nothing. */
  .nudge{position:fixed;top:60px;right:16px;z-index:45;display:none;flex-direction:column;gap:2px;
    max-width:250px;text-align:left;padding:9px 12px;border-radius:12px;cursor:pointer;font-family:inherit;
    background:var(--panel);border:1px solid var(--line-strong);box-shadow:var(--shadow-lg);
    opacity:0;transform:translateY(-6px);transition:opacity .22s ease,transform .22s var(--ease)}
  .nudge.on{display:flex;opacity:1;transform:none}
  .nudge::before{content:"";position:absolute;top:-5px;right:var(--caret-right,16px);
    width:9px;height:9px;background:var(--panel);border-left:1px solid var(--line-strong);
    border-top:1px solid var(--line-strong);transform:rotate(45deg)}
  .nudge .nudge-t{color:var(--text);font-size:.8125rem;font-weight:700;line-height:1.35}
  .nudge .nudge-s{color:var(--muted);font-size:.6875rem;line-height:1.45}
  .nudge:hover{border-color:var(--brand)}
  .nudge:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
  /* the button being pointed at breathes three times, then stops */
  .iconbtn.nudged{animation:nudgeRing 2.2s ease-out 3}
  @keyframes nudgeRing{
    0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--brand) 60%,transparent)}
    60%{box-shadow:0 0 0 11px transparent}
    100%{box-shadow:0 0 0 0 transparent}}
  @media (prefers-reduced-motion:reduce){
    .nudge{transition:none}
    .iconbtn.nudged{animation:none;border-color:var(--brand)}
  }
  @media (max-width:420px){ .nudge{max-width:min(250px,calc(100vw - 32px))} }
  /* =============== THE LED BOARD ===============
     A dark strip with one line travelling across it.
     DIRECTION: the text enters at the right and leaves at the left, which is
     what an airport board and Tales Runner both do — you read a line as it
     arrives instead of backwards. To flip it, swap the two keyframe transforms
     below and change padding-left to padding-right on .led-track. */
  .led{position:relative;display:flex;align-items:center;gap:8px;
    height:42px;padding:0 4px 0 10px;overflow:hidden;
    background:#0a0c10;border-bottom:1px solid #1d2330;
    box-shadow:inset 0 -6px 14px rgba(0,0,0,.55);
    animation:ledDrop .32s var(--ease) both}
  /* A class sets display here, which OUTRANKS the UA stylesheet's
     [hidden]{display:none} — so the bar ignored its own hidden attribute and
     never went away: not when the × was pressed, and not on a page with no
     announcement at all, where it sat at the top as an empty black strip.
     Same trap .bell-badge[hidden] and .ver-badge[hidden] already exist to
     avoid; anything that hides itself by attribute needs this line. */
  .led[hidden]{display:none}
  @keyframes ledDrop{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}
  .led-badge{flex:none;display:inline-flex;position:relative;z-index:1;
    color:#ffb02e;filter:drop-shadow(0 0 4px rgba(255,176,46,.6))}
  .led-win{flex:1;min-width:0;overflow:hidden;position:relative;z-index:1}
  /* Sized and spaced for Thai, which is what this board actually carries.
     line-height is a RATIO, not the bar's own height: at 38px flat the line box
     was exactly the strip, so วรรณยุกต์ above and สระ below the baseline were
     being clipped by the overflow. Tracking is near zero for the same reason —
     letter-spacing pushes a combining mark away from the consonant it belongs
     to, which is what turned ที่ into two separate shapes. 700 rather than 800:
     only 400-700 of Sarabun is loaded, so 800 was being faked, and a synthesised
     bold closes up the counters at this size. */
  .led-track{display:inline-block;white-space:nowrap;padding-left:100%;
    font-weight:700;font-size:1.0625rem;letter-spacing:.01em;line-height:1.7;
    color:#ffce63;text-shadow:0 0 4px rgba(255,176,46,.5),0 0 13px rgba(255,140,0,.2);
    animation:ledRun 20s linear infinite}
  @keyframes ledRun{from{transform:translateX(0)}to{transform:translateX(-100%)}}
  /* THE LAMP GRID, under the text rather than over it. It used to be a 55%
     black veil laid across the letters, and since the panel behind it is
     already black the grid was only ever visible BY darkening the text — which
     is precisely what made a 15px Thai line hard to read. Turned the other way
     up it is the unlit lamps of the panel (faint amber on black) with the
     letters as the lit ones on top, so the board still reads as a lamp panel
     and the text is not being scribbled on. */
  .led::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:0;
    background-image:radial-gradient(circle at 50% 50%, rgba(255,176,46,.085) 0 1px, transparent 1.5px);
    background-size:4px 4px}
  .led-x{flex:none;position:relative;z-index:1;background:none;border:none;cursor:pointer;
    color:#7d8698;padding:6px;line-height:0;border-radius:8px}
  .led-x:hover{color:#ffc14d}
  .led-x:focus-visible{outline:2px solid #ffc14d;outline-offset:-2px}
  /* stop it so a long line can actually be read */
  .led:hover .led-track,.led:focus-within .led-track{animation-play-state:paused}
  @media (prefers-reduced-motion:reduce){
    .led{animation:none}
    .led-track{animation:none;padding-left:0;overflow:hidden;text-overflow:ellipsis;
      max-width:100%;display:block}
  }
  @media (max-width:520px){
    .led{height:38px}
    .led-track{font-size:.9375rem}   /* the ratio line-height above still applies */
  }
  /* the same panel, shrunk, as the preview inside the compose modal */
  .led.led-mini{height:38px;border-radius:10px;border:1px solid #1d2330;margin-top:6px}
  .led.led-mini .led-track{font-size:.9375rem}
  /* ---- WORK THAT IS ONLY ON THIS COMPUTER ----
     Two things can be in that state and both used to be invisible:
       · writes the server has not taken yet — the held count was drawn INSIDE
         the offline banner, which is display:none whenever the browser thinks
         it is online, and a write can now be held while perfectly online (one
         that was never answered)
       · a form somebody was typing into and did not save
     They are one bar because they are one question — "is anything I did still
     only here?" — and the answer has to be somewhere the eye lands without
     being asked to look. */
  /* Mixed with the page background rather than left transparent: this bar is
     stuck to the top while the list scrolls UNDER it, and a 16% tint let every
     row travelling past show through the words. */
  .unsaved-bar{display:none;align-items:center;gap:var(--sp-3);padding:8px 14px;
    background:color-mix(in srgb,var(--amber) 16%,var(--bg));
    border-bottom:1px solid color-mix(in srgb,var(--amber) 32%,transparent);
    color:var(--amber);font-size:.8125rem;font-weight:600;
    flex-wrap:wrap;justify-content:center}
  .unsaved-bar.on{display:flex;animation:fadeIn .2s ease both}
  .unsaved-bar .ic{width:16px;height:16px;flex:none;color:var(--amber)}
  .unsaved-bar .uns-what{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
  .unsaved-bar .uns-sep{opacity:.5}
  /* What was half-typed, not merely THAT something was: the title and how long
     ago it was touched — a notice you can act on without opening anything.
     Not faded back, either: the title IS the content of the notice, and in the
     light palette the bar's amber is already only ~4:1 against its own tint. */
  .unsaved-bar .uns-quote{display:inline-block;vertical-align:bottom;
    margin-left:6px;font-weight:600;
    max-width:min(46ch,52vw);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .unsaved-bar button{font-family:inherit;font-size:.75rem;font-weight:700;cursor:pointer;
    padding:3px 11px;border-radius:20px;white-space:nowrap;
    border:1px solid color-mix(in srgb,var(--amber) 45%,transparent);
    background:color-mix(in srgb,var(--amber) 12%,transparent);color:var(--amber);
    transition:background var(--dur-fast) var(--ease)}
  .unsaved-bar button:hover{background:color-mix(in srgb,var(--amber) 26%,transparent)}
  .unsaved-bar button.plain{border-color:transparent;background:transparent;
    text-decoration:underline;font-weight:600}
  @media (max-width:520px){
    .unsaved-bar{gap:var(--sp-2);padding:7px 10px;font-size:.75rem}
  }
  .offline-banner{display:none;align-items:center;gap:8px;padding:8px 14px;
    background:color-mix(in srgb,var(--amber) 18%,var(--bg));
    border-bottom:1px solid color-mix(in srgb,var(--amber) 30%,transparent);
    color:var(--amber);font-size:.8125rem;font-weight:600;text-align:center;
    justify-content:center}
  .offline-banner.on{display:flex;animation:fadeIn .2s ease both}
  .offline-banner .ic{width:16px;height:16px;color:var(--amber)}

  /* ---- ICONS: one family (Lucide), one stroke width, sizes from tokens ---- */
  .ic{width:var(--icon-sm);height:var(--icon-sm);flex:0 0 auto;display:inline-block;
      vertical-align:-.18em;stroke:currentColor;fill:none;stroke-width:1.75;
      stroke-linecap:round;stroke-linejoin:round}
  .ic.md{width:var(--icon-md);height:var(--icon-md)}
  .ic.lg{width:var(--icon-lg);height:var(--icon-lg)}
  #sprite{position:absolute;width:0;height:0;overflow:hidden}

  /* thin request-activity bar */
  #netbar{position:fixed;top:0;left:0;height:2px;width:100%;z-index:60;background:transparent;pointer-events:none}
  #netbar>i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--brand),var(--violet));
    transition:width .35s ease,opacity .4s ease;opacity:0}
  #netbar.on>i{width:85%;opacity:1}
  #netbar.done>i{width:100%;opacity:0}

  /* ambient light: slow-drifting orbs behind the glass (decorative, never hit-testable).
     Colours derive from the registered tokens, so they follow palette + theme and
     cross-fade on switch; prefers-reduced-motion freezes them at their base spot. */
  #ambient{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;
    /* static brand-tinted washes (previously on body) — a fixed layer is anchored
       to the viewport, so the colour never scrolls away, stops short of the fold,
       or breaks into mismatched bands. Invalid without color-mix: plain --bg. */
    background-image:
      radial-gradient(1100px 720px at 88% -12%,color-mix(in srgb,var(--brand) var(--orb-a),transparent),transparent 62%),
      radial-gradient(940px 660px at -10% 22%,color-mix(in srgb,var(--brand2) var(--orb-b),transparent),transparent 60%),
      radial-gradient(820px 600px at 55% 112%,color-mix(in srgb,var(--violet) var(--orb-c),transparent),transparent 58%)}
  #ambient .blob{position:absolute;border-radius:50%;will-change:transform}
  #ambient .b1{width:62vmax;height:62vmax;left:-20vmax;top:-20vmax;
    background:radial-gradient(circle,color-mix(in srgb,var(--brand) var(--orb-a),transparent) 0%,transparent 66%);
    animation:drift1 54s ease-in-out infinite alternate}
  #ambient .b2{width:54vmax;height:54vmax;right:-16vmax;bottom:-16vmax;
    background:radial-gradient(circle,color-mix(in srgb,var(--brand2) var(--orb-b),transparent) 0%,transparent 64%);
    animation:drift2 70s ease-in-out infinite alternate}
  #ambient .b3{width:36vmax;height:36vmax;left:36%;top:58%;
    background:radial-gradient(circle,color-mix(in srgb,var(--violet) var(--orb-c),transparent) 0%,transparent 60%);
    animation:drift3 46s ease-in-out infinite alternate}
  @keyframes drift1{from{transform:translate3d(0,0,0)}to{transform:translate3d(10vmax,7vmax,0)}}
  @keyframes drift2{from{transform:translate3d(0,0,0)}to{transform:translate3d(-9vmax,-6vmax,0)}}
  @keyframes drift3{from{transform:translate3d(0,0,0)}to{transform:translate3d(6vmax,-8vmax,0)}}

  /* ONE STICKY BLOCK, NOT FOUR COMPETING ONES.
     The navbar, the announcement board, the unsaved bar and the offline banner
     each had position:sticky;top:0 of their own. Unscrolled they sat in a tidy
     column, which is the only state they were ever looked at in — but the
     moment the page moved they all stuck to the SAME offset and piled up on
     each other. The amber bar outranked the navbar by z-index, so it was drawn
     straight across the tabs, the level chip and the avatar, and neither could
     be read. They belong together: the group sticks, and inside it they queue
     up exactly as they do at the top of the page. */
  .topstack{position:sticky;top:0;z-index:20}
  /* z-index above its siblings in the stack: the ⋯ overflow panel hangs out of
     the navbar and down over whatever is below it, and the bar underneath was
     painting over the top of it. */
  header.top{position:relative;z-index:1;background:var(--header);
    background:color-mix(in srgb,var(--bg) var(--glass-a2),transparent);
    -webkit-backdrop-filter:blur(18px) saturate(1.6);backdrop-filter:blur(18px) saturate(1.6);
    border-bottom:1px solid var(--line);box-shadow:var(--glass-in),var(--glass-drop)}
  .topin{max-width:1180px;margin:0 auto;padding:12px 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
  .logo{font-weight:700;font-size:16px;letter-spacing:.2px}.logo span{color:var(--brand)}
  [data-theme="light"] .logo span{color:var(--brand2)}
  .roletabs{display:flex;background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(12px) saturate(1.4);backdrop-filter:blur(12px) saturate(1.4);
    border:1px solid var(--line);border-radius:10px;padding:3px;
    position:relative;gap:2px;flex-wrap:wrap}
  .roletabs button{background:none;border:none;color:var(--muted);padding:0 14px;border-radius:8px;cursor:pointer;
    font-size:.8125rem;font-weight:600;font-family:inherit;min-height:var(--tap);
    display:inline-flex;align-items:center;gap:6px;position:relative;z-index:1;
    transition:color var(--dur-fast) ease,background-color var(--dur-fast) ease,transform 120ms ease}
  .roletabs button:hover{color:var(--text)}
  .roletabs button:active{transform:scale(.96)}
  .roletabs button.on{background:var(--brand);color:var(--on-brand);box-shadow:var(--shadow-sm)}
  /* Sliding selection pill: instead of the brand background jumping from one tab
     to the next, ONE pill (::before) travels to the pressed tab. slideTabs()
     measures the .on button and writes --ink-x/y/w/h; the static .on background
     above stays the fallback until a group is actually measured (.inked), so a
     hidden, unmeasured or scripting-less group still shows its selection. */
  .roletabs::before{content:"";position:absolute;left:0;top:0;pointer-events:none;
    width:var(--ink-w,0);height:var(--ink-h,0);border-radius:8px;
    background:var(--brand);box-shadow:var(--shadow-sm);opacity:0;
    transform:translate(var(--ink-x,0),var(--ink-y,0));
    transition:transform 340ms cubic-bezier(.34,1.28,.5,1),width 340ms cubic-bezier(.34,1.28,.5,1),
               height 200ms var(--ease),opacity 160ms ease}
  .roletabs.inked::before{opacity:1}
  .roletabs.inked button.on{background:none;box-shadow:none}
  /* a re-measure (first paint, resize, a group being revealed) must land the pill
     where it belongs instead of sliding in from the corner */
  .roletabs.ink-jump::before{transition:none}
  /* The profile switch takes the far end of the row the view tabs are in. When
     the row is too narrow for both it wraps onto its own line at full width
     instead of squeezing รายการ/ปฏิทิน/ตารางทีม down to their icons. */
  .va-tabs{margin-left:auto}
  @media (max-width:560px){
    .va-tabs{margin-left:0;width:100%}
    .va-tabs button{flex:1;justify-content:center;padding:0 8px}
  }
  /* team-schedule person filter: multi-select pills (brand-filled = shown) */
  /* The signed-in identity block, pushed to the far end of the top bar. Scoped
     to .topin: as a bare .who it also matched the mention dropdown's rows and
     the AI review header — both of which nest their own .who and neither of
     which reset margin-left, so this auto margin leaked in and shoved their
     names into the middle of the row (two auto margins splitting the free
     space) instead of leaving them left-aligned. */
  .topin>.who{margin-left:auto;display:flex;align-items:center;gap:10px;color:var(--muted);font-size:.8125rem;
    position:relative}   /* the containing block for .who-pop — see its rule */
  .avatar{width:32px;height:32px;border-radius:50%;background:var(--brand);display:grid;place-items:center;
    font-weight:700;font-size:.75rem;color:var(--on-brand);box-shadow:var(--shadow-sm)}
  .avatar.pic{font-size:17px;line-height:1}   /* an emoji character fills the disc better than an initial */
  button.avatar{cursor:pointer;border:none;padding:0;font-family:inherit;
    transition:transform var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  button.avatar:hover{transform:scale(1.07);box-shadow:var(--shadow)}
  button.avatar:active{transform:scale(.93)}
  .avatar.lg{width:56px;height:56px;font-size:1.6rem;flex:none}
  .avatar.lg.pic{font-size:30px}
  .avatar.sm{width:22px;height:22px;font-size:.6rem;flex:none;box-shadow:none;background:var(--brand2)}
  .avatar.sm.pic{font-size:13px}
  .who-edit{background:none;border:none;padding:0;font-family:inherit;font-size:inherit;color:inherit;
    cursor:pointer;transition:color var(--dur-fast) ease}
  .who-edit:hover{color:var(--brand)}
  /* avatar + name merged into one click target (both used to open the same
     profile modal as two separate buttons) */
  .who-btn{display:flex;align-items:center;gap:8px;background:none;border:none;
    padding:2px 10px 2px 2px;border-radius:20px;cursor:pointer;font-family:inherit;
    transition:background-color var(--dur-fast) ease,transform 120ms ease}
  .who-btn:hover{background:color-mix(in srgb,var(--panel2) 70%,transparent)}
  .who-btn:hover .who-edit{color:var(--brand)}
  .who-btn:active{transform:scale(.97)}
  /* link to the external WFH calendar site — keeps a visible label (unlike the
     icon-only utility buttons beside it) since "goes to another website" isn't
     obvious from an icon alone; the small arrow marks it as external */
  .wfhbtn{display:inline-flex;align-items:center;gap:6px;height:var(--tap);min-height:36px;
    padding:0 12px;border-radius:20px;font-family:inherit;font-size:.8125rem;font-weight:600;
    white-space:nowrap;text-decoration:none;color:var(--text);cursor:pointer;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.4);backdrop-filter:blur(10px) saturate(1.4);
    border:1px solid var(--line-strong);box-shadow:var(--glass-drop);
    transition:transform var(--dur-fast) ease,background-color var(--dur-fast) ease,border-color var(--dur-fast) ease}
  .wfhbtn:hover{border-color:var(--brand);color:var(--brand);
    background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent)}
  .wfhbtn:active{transform:scale(.97)}
  .wfhbtn .ext{width:12px;height:12px;opacity:.6}
  /* ---- navbar overflow: only what does not fit leaves the bar ----
     Which buttons are in the bar differs per account, and TH/EN labels are
     different widths, so no breakpoint can predict when it stops fitting —
     whoFits() measures the live bar and moves the tail into .who-pop one
     button at a time until the header is back to a single row. */
  .who-sec{display:flex;align-items:center;gap:10px}
  .who-more{display:none;background:none;border:none;color:var(--muted);font-size:1.1rem;
    cursor:pointer;padding:4px 6px;border-radius:8px;line-height:1;
    transition:color var(--dur-fast) ease,background var(--dur-fast) ease}
  .who-more:hover{color:var(--text);background:color-mix(in srgb,var(--panel2) 60%,transparent)}
  /* the ⋯ exists only once something is actually behind it */
  .topin>.who.has-more .who-more{display:grid;place-items:center}
  /* Last resort, after every secondary button has already moved out: drop the
     name beside the avatar. The disc on its own still opens the profile, and a
     name is worth less than the second row of chrome it was costing. */
  .topin>.who.tight .who-edit{display:none}
  /* THE PANEL, anchored to .who — which is why .who is position:relative.
     An absolute box resolves against the nearest POSITIONED ancestor, and
     .topin is not one, so right:0 used to mean the right edge of the
     full-width sticky header. .topin is capped at 1180 and centred, so on a
     1920 screen the panel opened roughly 370px to the right of the ⋯ it
     belongs to, hard against the window edge — which is what "I press it and
     nothing shows" actually was. */
  .who-pop{position:absolute;top:calc(100% + 10px);right:0;z-index:200;
    display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;
    gap:8px;max-width:196px;   /* four icon buttons to a row, then it wraps */
    background:var(--panel);border:1px solid var(--line-strong);border-radius:12px;
    padding:10px;box-shadow:var(--shadow);
    opacity:0;transform:translateY(-6px) scale(.97);pointer-events:none;
    /* visibility (not just opacity) so the closed panel's buttons drop out of
       the tab order and the accessibility tree — opacity alone traps
       keyboard focus in an invisible panel */
    visibility:hidden;
    transition:opacity .18s var(--ease),transform .18s var(--ease),visibility 0s linear .18s}
  .topin>.who.open .who-pop{opacity:1;transform:none;pointer-events:auto;visibility:visible;
    transition:opacity .18s var(--ease),transform .18s var(--ease),visibility 0s}
  /* profile picker */
  .profile-top{display:flex;align-items:center;gap:12px}
  .role-chip{display:inline-block;padding:4px 10px;border-radius:8px;font-size:.8125rem;font-weight:600;
    background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand2)}
  /* ============ CHECKBOX ============
     The native control cannot be themed past accent-color, which ignores the
     app's radius, glass and focus ring. appearance:none hands us the box while
     the element stays a real checkbox — the tick is a rotated ::after border,
     and :indeterminate flattens it into a dash for "some of these". */
  .chk{-webkit-appearance:none;appearance:none;position:relative;flex:none;margin:0;
    width:19px;height:19px;border-radius:6px;cursor:pointer;
    border:1.5px solid var(--line-strong);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    box-shadow:var(--glass-in);
    transition:background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),
               box-shadow var(--dur-fast) var(--ease),transform 110ms ease}
  .chk:hover{border-color:var(--brand)}
  .chk:active{transform:scale(.88)}
  .chk:focus-visible{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .chk::after{content:"";position:absolute;left:50%;top:47%;width:5px;height:9px;
    border:solid var(--on-brand);border-width:0 2px 2px 0;border-radius:1px;
    transform:translate(-50%,-55%) rotate(45deg) scale(.4);opacity:0;
    transition:opacity 110ms ease,transform 180ms cubic-bezier(.34,1.5,.5,1)}
  .chk:checked{background:var(--brand);border-color:var(--brand);
    box-shadow:0 2px 9px color-mix(in srgb,var(--brand) 34%,transparent)}
  .chk:checked::after{opacity:1;transform:translate(-50%,-55%) rotate(45deg) scale(1)}
  /* indeterminate wins over checked: both can be set at once */
  .chk:indeterminate{background:var(--brand);border-color:var(--brand)}
  .chk:indeterminate::after{opacity:1;width:9px;height:0;border-width:0 0 2px 0;
    transform:translate(-50%,-50%) rotate(0deg) scale(1)}
  .chk:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}
  .chk:disabled:hover{border-color:var(--line-strong)}
  /* The container is the SCROLLER now; each group brings its own grid. */
  .pics{max-height:216px;overflow-y:auto;overscroll-behavior:contain;padding:2px 4px 2px 2px}
  @media(max-width:520px){ .pics{max-height:190px} }
  .pic-grp+.pic-grp{margin-top:var(--sp-3)}
  /* sticky, so the heading you are under is always the one on screen */
  .pic-grp-h{position:sticky;top:-2px;z-index:1;padding:var(--sp-1) 0 var(--sp-2);
    font-size:.625rem;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
    color:var(--muted);background:var(--panel);
    background:color-mix(in srgb,var(--panel) 92%,transparent)}
  /* auto-fill, not a fixed eight. Eight columns of whatever is going spare is
     how a 766px stacked form got 90px emoji tiles — the count has to follow
     the width so the TILE stays the size a tile should be, and the grid gains
     a column instead of inflating the ones it has. */
  .pic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(54px,1fr));gap:6px}
  .pics .pic{border:1px solid var(--line);border-radius:10px;aspect-ratio:1;display:grid;place-items:center;
    font-size:1.3rem;line-height:1;cursor:pointer;background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    transition:transform var(--dur-fast) ease,border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .pics .pic:hover{transform:translateY(-1px);border-color:var(--line-strong)}
  .pics .pic.on{border-color:var(--brand);box-shadow:0 0 0 2px var(--ring);
    background:color-mix(in srgb,var(--brand) 14%,transparent)}
  .iconbtn{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.4);backdrop-filter:blur(10px) saturate(1.4);
    border:1px solid var(--line-strong);color:var(--text);box-shadow:var(--glass-drop);
    width:var(--tap);height:var(--tap);min-width:36px;min-height:36px;
    border-radius:10px;cursor:pointer;display:grid;place-items:center;line-height:1;
    transition:transform var(--dur-fast) ease,background-color var(--dur-fast) ease,border-color var(--dur-fast) ease}
  .iconbtn:hover{border-color:var(--brand);background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent)}
  .iconbtn:active{transform:scale(.92)}
  .iconbtn[aria-expanded="true"]{border-color:var(--brand);background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent)}
  /* language toggle shows a 2-letter code, so it needs to grow past the square icon box */
  .langbtn{width:auto;min-width:var(--tap);padding:0 10px;font-family:inherit;
    font-weight:700;font-size:.75rem;letter-spacing:.5px}

  /* ---- BELL (notifications) ---- */
  .bellbtn{position:relative}
  /* The feedback button also carries a .bell-badge, so it needs its own
     positioning context too — otherwise the badge anchors to the shared .who
     row and pokes past the right viewport edge on narrow screens. */
  .fbbtn{position:relative}
  .bell-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:9px;
    background:var(--red);color:#fff;font-size:.625rem;font-weight:700;line-height:16px;text-align:center;
    box-shadow:0 0 0 2px var(--bg);display:inline-block;animation:bell-pop .25s cubic-bezier(.3,1.4,.5,1)}
  .bell-badge[hidden]{display:none}
  @keyframes bell-pop{from{transform:scale(.4);opacity:.2}to{transform:scale(1);opacity:1}}
  .bell-pop{min-width:340px;max-width:380px}
  .bell-pop .bclear{background:none;border:1px solid var(--line-strong);color:var(--brand2);border-radius:8px;
    padding:3px 9px;font-size:.6875rem;font-family:inherit;cursor:pointer;margin-left:auto;white-space:nowrap;
    transition:border-color var(--dur-fast) ease,background-color var(--dur-fast) ease}
  .bell-pop .bclear:hover{background:color-mix(in srgb,var(--brand) 12%,transparent);border-color:var(--brand)}
  .bell-body .bempty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:20px 6px;
    color:var(--muted);font-size:.8125rem;text-align:center}
  .bell-body .bempty .ic{opacity:.6}
  .bsec-t{display:flex;align-items:center;gap:6px;font-size:.6875rem;font-weight:700;color:var(--muted);
    text-transform:uppercase;letter-spacing:.4px;margin:8px 0 3px;padding:0 4px}
  .bsec-t .ic{color:var(--brand)}
  .bell-row{display:flex;gap:10px;padding:8px;border-radius:10px;cursor:pointer;align-items:flex-start;
    transition:background-color var(--dur-fast) ease}
  .bell-row:hover{background:color-mix(in srgb,var(--brand) 10%,transparent)}
  .bell-row.new{background:color-mix(in srgb,var(--brand) 8%,transparent)}
  .bell-row.new:hover{background:color-mix(in srgb,var(--brand) 14%,transparent)}
  .bell-row .bic{width:30px;height:30px;border-radius:9px;flex:none;display:flex;align-items:center;justify-content:center;
    background:color-mix(in srgb,var(--brand) 14%,var(--panel2));color:var(--brand2)}
  .bell-row.urgent{border-left:3px solid var(--amber);border-radius:10px}
  .bell-row .bt{font-size:.8125rem;font-weight:600;color:var(--text);display:flex;gap:6px;align-items:flex-start;
    line-height:1.35;min-width:0}
  .bell-row .bt .tx{min-width:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;
    -webkit-box-orient:vertical}
  .bell-row .bm{font-size:.75rem;color:var(--muted);margin-top:2px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
  .bell-row .bm .tchip{background:var(--panel2);border:1px solid var(--line);border-radius:6px;
    padding:0 5px;font-size:.6875rem;color:var(--text)}
  .bell-row .bm .soon{color:var(--amber);font-weight:700}
  .dot-new{width:8px;height:8px;border-radius:50%;background:var(--red);flex:none;margin-top:5px;margin-left:auto;
    box-shadow:0 0 0 3px color-mix(in srgb,var(--red) 18%,transparent)}
  .bell-body{max-height:min(56vh,380px);overflow-y:auto;margin:0 -4px;padding:0 4px;scrollbar-width:thin;
    scrollbar-color:var(--line-strong) transparent}
  .bell-body::-webkit-scrollbar{width:6px}
  .bell-body::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:3px}

  /* ---- PALETTE PICKER ---- */
  .picker{position:relative}
  .pop{position:fixed;top:60px;right:16px;z-index:40;background:var(--panel);
    background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent);
    border:1px solid var(--line);border-radius:18px;padding:14px;box-shadow:var(--shadow),var(--glass-in);
    min-width:340px;max-width:368px;max-height:min(74vh,540px);overflow-y:auto;display:none;
    -webkit-backdrop-filter:blur(20px) saturate(1.5);backdrop-filter:blur(20px) saturate(1.5);
    scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}
  .pop::-webkit-scrollbar{width:6px}
  .pop::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:3px}
  .pop::-webkit-scrollbar-track{background:transparent}
  .pop.on{display:block;animation:popIn .18s var(--ease) both}
  .pop.off{display:block;animation:popOut .15s ease both;pointer-events:none}
  .pop-head{display:flex;align-items:center;gap:8px;margin-bottom:10px;padding-bottom:10px;
    border-bottom:1px solid var(--line)}
  .pop-head .ic{color:var(--brand)}
  .pop h4{font-size:.75rem;font-weight:600;color:var(--muted);text-transform:uppercase;
    letter-spacing:.4px;margin:0}
  .swatches{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
  .sw{background:transparent;border:none;border-radius:14px;cursor:pointer;
    padding:6px 4px 8px;display:flex;flex-direction:column;align-items:center;gap:7px;
    font-family:inherit;font-size:.6875rem;color:var(--muted);text-align:center;
    transition:transform var(--dur-fast) ease,color var(--dur-fast) ease}
  .sw:hover{transform:translateY(-1px);color:var(--text)}
  .sw .sw-name{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .sw[aria-pressed="true"]{color:var(--text);font-weight:600}
  /* each swatch previews its own palette: page colour as the tile, accent as the disc */
  .sw .dot{width:100%;height:38px;border-radius:12px;border:1px solid var(--line);
    position:relative;overflow:hidden;background:var(--sw-bg);
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .sw .dot::after{content:"";position:absolute;left:7px;top:50%;width:15px;height:15px;
    border-radius:50%;transform:translateY(-50%);background:var(--sw-accent);
    box-shadow:0 0 0 1px rgba(128,128,128,.3)}
  .sw:hover .dot{border-color:var(--line-strong)}
  .sw[aria-pressed="true"] .dot{border-color:var(--brand);box-shadow:0 0 0 2px var(--ring)}
  /* check badge on the active swatch */
  .sw[aria-pressed="true"] .dot::before{content:"✓";position:absolute;inset:0;z-index:1;
    display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;
    color:var(--on-brand);background:var(--sw-accent)}

  /* ---- THEMED SELECT (replaces a native <select>'s open option list with a
     web-rendered popover; the real <select> stays in the DOM, hidden, as the
     source of truth — see openSelPop()/syncSelBtn()) ---- */
  .selbtn{width:100%;display:inline-flex;align-items:center;justify-content:space-between;gap:8px;
    text-align:left;background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    color:var(--text);border:1px solid var(--line-strong);border-radius:8px;
    padding:0 10px;min-height:var(--tap);font-size:.875rem;font-family:inherit;cursor:pointer}
  .selbtn:focus-visible{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  /* ---- meeting length: four presets and the readout, under the time row ----
     The lengths are chips rather than a third dropdown because there are only
     four of them and each is one press; the readout beside them is the only
     place the form ever says what was actually chosen, so it keeps its own
     line rather than sharing with a label. */
  .durrow{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
  .durbtn{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    color:var(--muted);border:1px solid var(--line);border-radius:20px;
    padding:0 12px;min-height:32px;font-size:.75rem;font-weight:600;font-family:inherit;cursor:pointer;
    transition:color var(--dur-fast) ease,background-color var(--dur-fast) ease,transform 120ms ease}
  .durbtn:hover:not(:disabled){color:var(--text);border-color:var(--line-strong)}
  .durbtn:active:not(:disabled){transform:scale(.96)}
  .durbtn.on{background:var(--brand);color:var(--on-brand);border-color:transparent;box-shadow:var(--shadow-sm)}
  .durbtn:disabled{opacity:.5;cursor:default}
  .durnote{margin-left:auto;color:var(--muted);font-size:.75rem;font-variant-numeric:tabular-nums}
  .durnote.bad{color:var(--amber);font-weight:600}
  @media (max-width:400px){
    .durnote{margin-left:0;width:100%}
  }
  .selbtn .sel-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .selbtn .sel-caret{color:var(--muted);flex:none;transition:transform .16s ease}
  .selbtn[aria-expanded="true"] .sel-caret{transform:rotate(180deg)}
  .statsel.pill .sel-caret{width:12px;height:12px}
  .pop.select-pop{position:fixed;min-width:200px;max-width:280px;padding:6px;border-radius:14px;
    max-height:min(60vh,360px);overflow-y:auto;z-index:55}   /* above .modal (50) — selects live inside modals */
  .pop.select-pop:focus{outline:none}   /* programmatically focused for arrow-key nav; the highlighted .sel-opt row is the visible indicator */
  .select-pop .sel-group{font-size:.6875rem;font-weight:600;color:var(--muted);text-transform:uppercase;
    letter-spacing:.03em;padding:8px 10px 4px}
  .select-pop .sel-opt{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;
    border-radius:8px;cursor:pointer;font-size:.875rem;text-align:left;background:transparent;
    border:none;color:var(--text);font-family:inherit}
  .select-pop .sel-opt .ic{flex:none;color:var(--brand);visibility:hidden}
  .select-pop .sel-opt[aria-selected="true"] .ic{visibility:visible}

  /* ---- COMMAND PALETTE (Ctrl+K) ---- VS Code-style overlay with fuzzy search */
  .cmdpal-overlay{position:fixed;inset:0;z-index:200;display:none;align-items:flex-start;justify-content:center;
    padding-top:min(20vh,160px);background:rgba(0,0,0,.45);backdrop-filter:blur(4px)}
  .cmdpal-overlay.on{display:flex;animation:fadeIn .12s ease both}
  .cmdpal-overlay.off{display:flex;animation:fadeOut .1s ease both;pointer-events:none}
  .cmdpal{width:min(520px,92vw);max-height:min(70vh,480px);display:flex;flex-direction:column;
    background:var(--panel);background:color-mix(in srgb,var(--panel) calc(var(--glass-a3) + 7%),transparent);
    -webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);
    border:1px solid var(--line);border-radius:14px;overflow:hidden;
    box-shadow:0 24px 64px rgba(0,0,0,.45),inset 0 1px 0 color-mix(in srgb,#fff 12%,transparent);
    animation:popIn .15s var(--ease) both}
  .cmdpal-search{display:flex;align-items:center;gap:10px;padding:12px 14px;
    border-bottom:1px solid var(--line)}
  .cmdpal-search .ic{color:var(--muted);flex:none}
  .cmdpal-input{flex:1;background:transparent;border:none;outline:none;color:var(--text);
    font-size:.9375rem;font-family:inherit;padding:0}
  .cmdpal-input::placeholder{color:var(--muted)}
  .cmdpal-esc{font-size:.6875rem;padding:2px 6px;border-radius:4px;border:1px solid var(--line);
    color:var(--muted);background:var(--panel2);font-family:inherit}
  .cmdpal-list{flex:1;overflow-y:auto;padding:6px;scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}
  .cmdpal-list::-webkit-scrollbar{width:5px}
  .cmdpal-list::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:3px}
  .cmdpal-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;
    cursor:pointer;transition:background 80ms ease}
  .cmdpal-item:hover,.cmdpal-item.active{background:color-mix(in srgb,var(--brand) 12%,transparent)}
  .cmdpal-item .ic{flex:none;color:var(--muted);width:18px;height:18px}
  .cmdpal-item.active .ic,.cmdpal-item:hover .ic{color:var(--brand)}
  .cmdpal-item .cmdpal-label{flex:1;font-size:.875rem;color:var(--text);overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap}
  .cmdpal-item .cmdpal-kbd{flex:none;font-size:.6875rem;padding:2px 6px;border-radius:4px;
    border:1px solid var(--line);color:var(--muted);background:var(--panel2);font-family:inherit;
    white-space:nowrap}
  .cmdpal-item .cmdpal-role{flex:none;font-size:.625rem;padding:1px 5px;border-radius:3px;
    background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand2);font-weight:600}
  .cmdpal-item mark{background:color-mix(in srgb,var(--amber) 30%,transparent);color:var(--text);
    border-radius:2px;padding:0 1px}
  .cmdpal-foot{display:flex;gap:14px;padding:8px 14px;border-top:1px solid var(--line);
    font-size:.6875rem;color:var(--muted)}
  .cmdpal-hint{display:flex;align-items:center;gap:4px}
  .cmdpal-hint kbd{font-size:.625rem;padding:1px 4px;border-radius:3px;border:1px solid var(--line);
    background:var(--panel2);font-family:inherit}
  .cmdpal-empty{padding:24px;text-align:center;color:var(--muted);font-size:.875rem}
  .cmdpal-section{font-size:.6875rem;font-weight:600;color:var(--muted);text-transform:uppercase;
    letter-spacing:.04em;padding:8px 10px 4px;user-select:none}
  .cmdpal-sep{height:1px;margin:4px 8px;background:var(--line)}
  /* History items: show Undo button on hover */
  .cmdpal-history .cmdpal-undo-btn{display:none;margin-left:auto;padding:2px 8px;font-size:.6875rem;
    border:1px solid var(--amber);color:var(--amber);border-radius:6px;background:transparent;
    cursor:pointer;font-weight:600;white-space:nowrap;transition:background var(--dur-fast) ease}
  .cmdpal-history:hover .cmdpal-undo-btn,.cmdpal-history.active .cmdpal-undo-btn{display:inline-flex}
  .cmdpal-history .cmdpal-undo-btn:hover{background:color-mix(in srgb,var(--amber) 16%,transparent)}
  /* Error Log modal */
  /* the recovery tray — every entry still holds the text it is about, so it is
     laid out to be READ and copied, not just counted */
  .tray-body{max-height:min(60vh,460px);overflow-y:auto;padding:2px 4px;
    scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}
  .tray-body::-webkit-scrollbar{width:5px}
  .tray-body::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:3px}
  .tray-intro{margin:0 0 10px;font-size:.8125rem;color:var(--muted);line-height:1.6}
  .tray-row{padding:10px 12px;margin-bottom:8px;border-radius:10px;
    border:1px solid color-mix(in srgb,var(--amber) 34%,var(--line));
    background:color-mix(in srgb,var(--amber) 7%,transparent)}
  .tray-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
  .tray-head b{font-size:.875rem;word-break:break-word}
  .tray-when{margin-left:auto;color:var(--muted);font-size:.6875rem;font-variant-numeric:tabular-nums}
  .tray-why{margin-top:3px;font-size:.75rem;color:var(--amber);font-weight:600}
  .tray-desc{margin-top:6px;font-size:.8125rem;color:var(--text);white-space:pre-wrap;
    word-break:break-word;max-height:9em;overflow-y:auto;opacity:.9}
  .tray-acts{display:flex;gap:6px;margin-top:9px;flex-wrap:wrap}
  .unsaved-bar.bad{background:color-mix(in srgb,var(--red) 14%,var(--bg))}
  .unsaved-bar.bad .ic{color:var(--red)}
  .unsaved-bar .uns-bad{color:var(--red);font-weight:700}
  /* Light palette only. --amber and --red are tuned to read on a dark surface;
     on this bar's own pale tint they land at 3.9:1, which is under AA for text
     this size — and this is a bar whose whole job is to be read. Darkened here
     and nowhere else, so the palette everywhere else is left alone.
     After the .bad rules on purpose: same specificity, so source order is what
     decides which colour the icon ends up with. */
  [data-theme="light"] .unsaved-bar{color:color-mix(in srgb,var(--amber) 80%,#000)}
  [data-theme="light"] .unsaved-bar.bad{color:color-mix(in srgb,var(--red) 80%,#000)}
  [data-theme="light"] .unsaved-bar .ic,
  [data-theme="light"] .unsaved-bar button,
  [data-theme="light"] .unsaved-bar .uns-bad{color:inherit}
  .elog-body{max-height:min(55vh,400px);overflow-y:auto;padding:2px 4px;
    scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}
  .elog-body::-webkit-scrollbar{width:5px}
  .elog-body::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:3px}
  .elog-row{display:flex;gap:8px;padding:7px 8px;border-radius:6px;font-size:.8125rem;
    border-bottom:1px solid var(--line);align-items:baseline}
  .elog-row:last-child{border-bottom:none}
  .elog-time{flex:0 0 auto;color:var(--muted);font-size:.6875rem;font-variant-numeric:tabular-nums}
  .elog-ctx{flex:0 0 auto;padding:1px 6px;border-radius:4px;font-size:.6875rem;font-weight:600;
    background:color-mix(in srgb,var(--red) 14%,transparent);color:var(--red)}
  .elog-msg{flex:1;color:var(--text);word-break:break-word}
  /* ---- feedback modal: submitted-message list (Thanit-only view) ---- */
  .fb-row{padding:10px 12px;border-radius:10px;margin-bottom:8px;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);border:1px solid var(--line)}
  .fb-row:last-child{margin-bottom:0}
  .fb-meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:4px}
  .fb-meta b{font-size:.8125rem;color:var(--text)}
  .fb-meta span{font-size:.6875rem;color:var(--muted);white-space:nowrap}
  .fb-msg{font-size:.875rem;line-height:1.55;color:var(--text);white-space:pre-wrap;word-break:break-word}
  .fb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:6px}
  .fb-count{font-size:.6875rem;color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums}
  .fb-count.over{color:var(--red);font-weight:600}
  .fb-noanon{display:inline-flex;align-items:center;gap:4px;font-size:.6875rem;color:var(--muted);text-align:right}
  .fb-noanon .ic{width:12px;height:12px;flex:none}
  /* ---- reflection modal history rows: reuse .fb-row/.fb-meta, just add the
     per-question sub-lines (today / obstacles / tomorrow) inside one row ---- */
  .refl-field{font-size:.8125rem;line-height:1.55;color:var(--text);margin-top:4px;
    white-space:pre-wrap;word-break:break-word}
  .refl-field b{color:var(--muted);font-weight:600}
  /* ---- EXP BADGE ----
     One quiet pill in the header: shows the level only. The exact EXP count and
     progress live inside the leaderboard, so the header doesn't scream numbers
     at the user on every action. */
  .exp-badge{display:inline-flex;align-items:center;gap:4px;padding:0 7px;border-radius:20px;
    font-size:.6875rem;font-weight:600;cursor:pointer;white-space:nowrap;color:var(--muted);
    border:1px solid var(--line);background:var(--panel2);font-family:inherit;line-height:1;
    min-height:28px;transition:border-color .15s ease,color .15s ease;
    position:relative;overflow:visible}
  .exp-badge:hover{border-color:var(--brand);color:var(--text)}
  /* the rank mark sits inside the level chip, on the chip's own colour */
  .exp-lv .ic{width:13px;height:13px;margin-right:4px;vertical-align:-.16em}
  .exp-lv{font-weight:700;color:var(--lv,inherit)}
  /* ---- build stamp (one named person only) ----
     Deliberately quiet: it is a diagnostic, not a feature. Turns amber when
     the UI and the server report different builds, which is the case worth
     noticing (only one of the two files got pasted). */
  .ver-badge{display:inline-flex;align-items:center;gap:4px;padding:0 7px;min-height:22px;
    border-radius:20px;font-size:.625rem;font-weight:600;letter-spacing:.02em;
    font-variant-numeric:tabular-nums;white-space:nowrap;cursor:default;
    color:var(--muted);background:var(--panel2);border:1px dashed var(--line-strong)}
  /* .ver-badge sets display, which outranks the UA stylesheet's [hidden]{display:none}
     (a class beats an attribute selector) - without this the empty dashed pill
     renders in every top bar, for everyone. Same trap .bell-badge[hidden] above
     already exists to avoid. */
  .ver-badge[hidden]{display:none}
  .ver-badge.mismatch{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 55%,transparent);
    border-style:solid}
  .ver-badge .ic{width:11px;height:11px;flex:none}
  /* "+N" floats up off the capsule the instant EXP lands, and the capsule
     itself bumps — so the number that just changed is also the thing that
     visibly moves, instead of the badge silently repainting to a new value. */
  .exp-badge.bump{animation:expBadgeBump .42s ease}
  @keyframes expBadgeBump{35%{transform:scale(1.18);border-color:var(--green)}}
  /* The running total, beside the level. It used to live only in the badge's
     title attribute, so earning EXP changed nothing anybody could see until a
     level boundary was crossed - the reward loop was invisible by design
     accident. Tabular figures because expBadgeCount() ticks these digits up
     one frame at a time and proportional ones would shuffle sideways. */
  .exp-num{font-weight:700;color:var(--text);font-variant-numeric:tabular-nums}
  .exp-dot{color:var(--line-strong)}
  .exp-u{font-size:.5625rem;font-weight:700;letter-spacing:.04em;color:var(--muted)}
  /* =============== THE PROFILE HUB ===============
     Badges and the daily card used to be two more icons in a top bar that was
     already overflowing, each opening its own modal. Both are about ONE person
     — the one whose profile it is — so both live in the profile now, and the
     bar is two buttons lighter. The panel reads top to bottom as a page about
     you: who you are, what level that has made you, what you have collected,
     and the one bit of play the day offers. */
  .pf-hero{display:flex;align-items:center;gap:var(--sp-4);padding:var(--sp-3) var(--sp-4);border-radius:16px;
    border:1px solid var(--line);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    box-shadow:var(--glass-in)}
  .pf-hero .avatar.lg{box-shadow:0 6px 18px color-mix(in srgb,var(--brand) 30%,transparent)}
  .pf-id{min-width:0;flex:1}
  .pf-name{font-weight:700;font-size:1.0625rem;line-height:1.3;overflow-wrap:anywhere}
  .pf-nick{color:var(--brand2);font-weight:600;font-size:.8125rem}
  .pf-sub{color:var(--muted);font-size:.75rem;margin-top:var(--sp-1);word-break:break-all}
  /* TWO COLUMNS, so the whole page fits a laptop without scrolling. Stacked in
     one 600px column it ran 1113px against the 689px a 1366x768 screen gives a
     modal — the daily card, which is the one thing here you actually DO today,
     was 400px below the fold every time.
     Left is what you have collected, right is what today offers; the left pair
     is the taller of the two, which is why the card sits alone on the right
     rather than under the badges. Below the breakpoint it falls back to one
     column and scrolls, which is what a phone does with everything. */
  /* 94vh, not the generic 90. This is a page, not a confirm box: it earns the
     extra 30px of a 768-tall laptop, which is the difference between the album
     ending above the fold and the whole thing scrolling. */
  #profileModal .sheet{max-width:min(1000px,94vw);max-height:94vh}
  /* The team overview is sized like the profile, and for the same reason: at
     480px the level card, the totals and five departments were one long column
     with a scrollbar in the middle of it. The width is only worth having if it
     is used, so past the same breakpoint the card moves beside the list rather
     than on top of it — half a screen of rows either way. */
  #lbModal .sheet{max-width:min(1000px,94vw);max-height:94vh}
  .lb-cols{display:grid;grid-template-columns:1fr;gap:var(--sp-4)}
  .lb-col{min-width:0}
  @media (min-width:880px){
    .lb-cols{grid-template-columns:320px 1fr;gap:0 var(--sp-5);align-items:start}
    .lb-cols>.lb-col:nth-child(1){grid-area:1/1}
    .lb-cols>.lb-col:nth-child(2){grid-area:1/2/span 2/3}
    /* the footnote fills the room going spare beside the card rather than
       sitting under a list it has already been read past */
    .lb-cols>.lb-note{grid-area:2/1;margin-top:var(--sp-3);align-self:start}
  }
  /* THE EDIT FORM used to sit in a 640px box inside a 1000px sheet, leaving
     328px of the window empty down the right-hand side.
     Simply removing the cap is not the fix, and was never the disagreement:
     the cap went on because a form left to fill 1000px stretched the avatar
     picker's tiles to 120px each and put half a metre between the two name
     fields. Two columns USE the width instead of merely occupying it — who
     you are and what you are called on the left, the picker on the right,
     where it finally has the height to show most of the sixty faces at once
     rather than two and a half rows through a letterbox. */
  #pfEdit{max-width:none}
  /* No gap and no extra rhythm while it is ONE column: the fields already
     carry their own bottom margins, and adding a grid gutter on top of them
     put 28px into a layout that was already 3px over on a 768px window. The
     gutter is a COLUMN gutter — it belongs to the two-column case only. */
  .pf-form{display:grid;grid-template-columns:1fr;gap:0}
  .pf-fcol{min-width:0}
  .pf-fhint{margin-top:0}
  @media (min-width:880px){
    .pf-form{grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:var(--sp-5);align-items:stretch}
    /* the picker fills whatever height the fields beside it make */
    .pf-fpics{display:flex;flex-direction:column}
    .pf-fpics>.field{flex:1;display:flex;flex-direction:column;min-height:0;margin-bottom:0}
    /* Tall enough to be worth the column, never tall enough to push the sheet
       past its own 94vh cap — uncapped, nine rows of faces drove the panel to
       846px on a 900px screen and put the Save button below the fold, which is
       a worse bargain than a scrollbar in the picker. */
    .pf-fpics .pics{flex:1;min-height:260px;max-height:min(52vh,460px)}
    /* Side by side while the form is ONE column (there is 800px to share), one
       per row once it is two (the left column is 470px, and two 230px inputs in
       it is worse than two full-width ones). Keeping the .g2 is what stops the
       stacked layout growing a row and pushing an 860px window into a scroll. */
    .pf-fcol .g2{grid-template-columns:1fr}
    /* Sixty faces need eight rows and four fields need four, so the left column
       is always the shorter of the two. Its note drops to the floor rather than
       dangling under the last input with 190px of nothing beneath it: the slack
       then reads as a margin, which is what it is, instead of as a column that
       gave up half way down. */
    .pf-fcol:first-child{display:flex;flex-direction:column}
    .pf-fcol:first-child>.pf-fhint{margin-top:auto;padding-top:var(--sp-4)}
  }
  /* SPACING. Everything below is on the --sp-* scale the app already
     declares. It used to be 3/6/7/8/10/11/13/14/16/20, which is not a scale —
     it is ten separate guesses, and the eye reads that as sloppiness long
     before it can say why. Section rhythm is --sp-5, the gap between a
     heading and what it labels is --sp-3, and inside a tile it is --sp-1.
     Nothing here sets a number that is not one of those.
     The hero and the columns had NO gap between them at all: the panel ran
     straight into the first card with a 1px border for a seam. */
  /* Stacked, the grid gap IS the section rhythm — at --sp-4 the seam between
     the two columns read as a tighter join than the one between two sections
     of the same column, which says the wrong thing about what belongs to what. */
  .pf-cols{display:grid;grid-template-columns:1fr;gap:var(--sp-5);margin-top:var(--sp-5)}
  .pf-col{min-width:0}
  @media (min-width:880px){
    /* stretch, not start. The left column is two sections and the right is
       one, so left alone the halves end 28px apart and the sheet looks torn
       along the bottom. The card panel takes whatever height the badges make
       and centres itself inside it, so both columns finish on one line. */
    .pf-cols{grid-template-columns:1.02fr .98fr;gap:var(--sp-5);align-items:stretch}
    .pf-col{display:flex;flex-direction:column}
    .pf-col>.pf-sec:last-child{flex:1;display:flex;flex-direction:column}
    .pf-col>.pf-sec:last-child>#pfCard{flex:1;display:flex;flex-direction:column;justify-content:center}
  }
  .pf-sec{margin-top:var(--sp-5)}
  /* Between the two-column breakpoint and a full laptop the sheet is narrower,
     the shelf runs to three rows of badges instead of two, and that row costs
     more height than the window has left to give. The rhythm steps down one
     notch of the scale; the shelf does not lose a badge. */
  @media (min-width:880px) and (max-width:1200px){
    .pf-cols{gap:var(--sp-4);margin-top:var(--sp-4)}
    .pf-sec{margin-top:var(--sp-4)}
  }
  .pf-col>.pf-sec:first-child{margin-top:0}
  .pf-sec>h4{display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-3);
    font-size:.6875rem;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--muted)}
  .pf-sec>h4 .ic{width:13px;height:13px;color:var(--brand)}
  .pf-sec>h4 .tail{margin-left:auto;font-weight:600;letter-spacing:0;text-transform:none;
    color:var(--muted);font-size:.6875rem}

  /* ---- TEAM TABLE ROWS ----
     The left edge answers "does this concern me" before anything is read: the
     brand colour for a meeting you are in, a plain rule for one you are not.
     In the day view the section headings say the same thing, but the week view
     has no sections and used to paint every row identically. */
  .tm-row{border-left:3px solid var(--line-strong)}
  .tm-row.tm-in{border-left-color:var(--brand)}
  .tm-row.tm-shut{cursor:default}
  .tm-row.tm-shut:hover{border-color:var(--line);box-shadow:var(--glass-in),var(--glass-drop)}
  .tm-time{color:var(--brand2);font-weight:700}
  .tm-role{font-size:.5625rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
    color:var(--muted);padding:1px 6px;border-radius:20px;border:1px solid var(--line);
    white-space:nowrap;margin-left:var(--sp-1)}
  .tm-att{display:inline-flex;align-items:center;gap:var(--sp-1)}
  .tm-stack{display:inline-flex;align-items:center}
  /* overlapped, with a ring in the panel colour so the discs read as separate
     rather than as one smudge */
  .tm-stack .tm-face{display:inline-flex;margin-left:-7px;border-radius:50%;
    box-shadow:0 0 0 2px var(--panel2)}
  /* bigger than the row's other faces on purpose: overlapped discs lose a
     third of their width to the one in front, so at the meta line's 20px they
     came out as a row of coloured dots */
  .tm-stack .avatar.sm{width:26px;height:26px;font-size:.6875rem}
  .tm-stack .avatar.sm.pic{font-size:15px}
  .tm-stack .tm-face:first-child{margin-left:0}
  .tm-stack .tm-face.me{box-shadow:0 0 0 2px var(--panel2),0 0 0 3.5px var(--brand);z-index:1}
  .tm-more{font-size:.6875rem;font-weight:700;color:var(--muted);font-variant-numeric:tabular-nums}
  .tm-lock{margin-left:auto;color:var(--muted);display:inline-flex;align-items:center}
  .tm-lock .ic{width:13px;height:13px}
  /* ---- BADGE SHELF ---- */
  /* Sixteen of these have to sit in half the sheet without pushing the fold
     down, so the tile is as small as it can be while the name still reads. */
  /* 52, not 56: the tile floor and the gap together decide the column count,
     and sixteen badges have to come out as two rows of eight. At 56 with an
     8px gap only seven fit, the album spills to a third row, and the sheet
     stops fitting the laptop it was widened to fit. */
  .badge-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:var(--sp-2)}
  /* the tiles are buttons now — without these they arrive with the user
     agent's own font, colour and centre alignment */
  button.badge-item{font:inherit;color:inherit;cursor:pointer;text-align:center}
  .badge-item{position:relative;display:flex;flex-direction:column;align-items:center;
    justify-content:flex-start;gap:var(--sp-1);
    padding:var(--sp-2) var(--sp-1);border-radius:12px;overflow:hidden;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    border:1px solid var(--line);box-shadow:var(--glass-in);
    transition:transform .18s var(--ease),border-color .18s ease,box-shadow .18s ease}
  .badge-item:hover{transform:translateY(-2px);border-color:var(--brand);
    box-shadow:var(--shadow-sm),var(--glass-in)}
  /* Locked is dimmed, not hidden: the point of an album is seeing what is still
     out there. grayscale alone left the emoji readable enough to look earned. */
  .badge-item.locked{opacity:.45;filter:grayscale(1) contrast(.85)}
  .badge-item.locked:hover{opacity:.7}
  .badge-icon{font-size:1.35rem;line-height:1.1;filter:drop-shadow(0 2px 4px rgba(0,0,0,.25))}
  .badge-name{font-size:.5625rem;font-weight:600;color:var(--text);text-align:center;line-height:1.2;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .badge-rarity{font-size:.5rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
  /* --accent and --border are not tokens in this app (the palette defines
     --brand/--brand2/--line/...), so "rare" was resolving to nothing and
     rendered in plain body text — indistinguishable from an ordinary badge. */
  .badge-rarity.common{color:var(--muted)}
  .badge-rarity.rare{color:var(--brand2)}
  .badge-rarity.legendary{color:var(--amber)}
  .badge-item.legendary:not(.locked){border-color:color-mix(in srgb,var(--amber) 45%,transparent);
    background:color-mix(in srgb,var(--amber) 8%,var(--panel2))}
  .badge-item.rare:not(.locked){border-color:color-mix(in srgb,var(--brand) 40%,transparent)}
  /* The grid already makes every tile in a row the same height; what it does
     not do is line up what is INSIDE them. "Planner" is one line and
     "Reflection Pro" is two, so their dates and padlocks floated 11px apart
     and the bottom of the shelf zig-zagged. margin-top:auto drops the marker
     to the floor of whatever tile it is in — one line across the row, and no
     empty second line reserved on the phone, where no name wraps at all. */
  .badge-earned-at,.badge-locked-mark{margin-top:auto}
  /* dd-mm-yyyy is ten characters where "07-30" was five: 41px against the 43px
     the tile's own side padding leaves. Spanning the padding gives it the full
     51px instead, which is room rather than a squeeze — and tabular figures
     keep the column of dates down the shelf aligned. */
  .badge-earned-at{font-size:.5rem;color:var(--green);font-variant-numeric:tabular-nums;
    width:100%;text-align:center;margin-inline:calc(var(--sp-1) * -1);
    white-space:nowrap;overflow:hidden}
  .badge-locked-mark{font-size:.5rem;color:var(--muted)}
  .badge-count{font-size:.75rem;color:var(--muted);margin-bottom:8px}
  /* the progress rail under the shelf: what proportion of the album is filled */
  .badge-bar{height:5px;border-radius:3px;margin-bottom:var(--sp-3);overflow:hidden;
    background:color-mix(in srgb,var(--line) 55%,transparent)}
  .badge-bar i{display:block;height:100%;border-radius:3px;
    background:linear-gradient(90deg,var(--brand),var(--brand2));transition:width .8s var(--ease)}
  /* one sweep of light across a badge the moment it is earned */
  .badge-item.fresh::after{content:"";position:absolute;inset:0;pointer-events:none;
    background:linear-gradient(105deg,transparent 30%,color-mix(in srgb,#fff 40%,transparent) 50%,transparent 70%);
    transform:translateX(-120%);animation:badgeSweep 1.1s var(--ease) .15s 2}
  @keyframes badgeSweep{to{transform:translateX(120%)}}
  .badge-item.fresh{animation:badgePop .5s cubic-bezier(.34,1.56,.64,1) both}
  @keyframes badgePop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.12)}100%{transform:scale(1)}}

  /* ---- one badge, in full ---- */
  .bd-top{display:flex;align-items:center;gap:var(--sp-4)}
  .bd-face{flex:none;width:76px;padding:var(--sp-3) var(--sp-2);cursor:default}
  .bd-face .badge-icon{font-size:2.2rem}
  .bd-face:hover{transform:none}
  .bd-id{min-width:0}
  .bd-name{font-size:1.0625rem;font-weight:700;line-height:1.3;overflow-wrap:anywhere}
  .bd-tags{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-2)}
  .bd-rar{font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;
    padding:2px 9px;border-radius:20px;border:1px solid var(--line)}
  .bd-rar.common{color:var(--muted)}
  .bd-rar.rare{color:var(--brand2);border-color:color-mix(in srgb,var(--brand) 40%,transparent);
    background:color-mix(in srgb,var(--brand) 10%,transparent)}
  .bd-rar.legendary{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 45%,transparent);
    background:color-mix(in srgb,var(--amber) 10%,transparent)}
  .bd-exp{font-size:.6875rem;font-weight:700;color:var(--green);
    padding:2px 9px;border-radius:20px;
    background:color-mix(in srgb,var(--green) 12%,transparent);
    border:1px solid color-mix(in srgb,var(--green) 26%,transparent)}
  .bd-row{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-4);
    padding:var(--sp-3);border-radius:12px;font-size:.8125rem;font-weight:600;
    border:1px solid var(--line)}
  .bd-row .ic{width:15px;height:15px;flex:none}
  .bd-row.got{color:var(--green);background:color-mix(in srgb,var(--green) 9%,transparent);
    border-color:color-mix(in srgb,var(--green) 24%,transparent)}
  .bd-row.not{color:var(--muted)}
  .bd-how{margin-top:var(--sp-4)}
  .bd-how-h{font-size:.6875rem;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
    color:var(--muted);margin-bottom:var(--sp-2)}
  .bd-how-b{font-size:.875rem;line-height:1.6}
  .bd-how-b.secret{color:var(--muted);font-style:italic}
  /* ---- THE DECK ----
     A real card, not a square with an emoji in it: two faces on one element,
     back showing until it turns. transform-style:preserve-3d plus a
     backface-hidden pair is the only way to get a flip that reveals rather
     than cross-fades. */
  .deck{display:flex;justify-content:center;align-items:center;gap:12px;flex-wrap:wrap;
    perspective:900px;min-height:120px;margin:2px 0 8px}
  .pcard{width:84px;height:118px;flex:none;position:relative;border:none;padding:0;
    background:none;font:inherit;cursor:default;transform-style:preserve-3d;
    transition:transform .45s var(--ease),filter .25s ease}
  .pcard-in{position:absolute;inset:0;transform-style:preserve-3d;
    transition:transform .62s cubic-bezier(.2,.85,.3,1)}
  .pcard.flip .pcard-in{transform:rotateY(180deg)}
  .pcard-face{position:absolute;inset:0;border-radius:13px;display:grid;place-items:center;
    -webkit-backface-visibility:hidden;backface-visibility:hidden;
    border:1px solid var(--line-strong);box-shadow:var(--shadow-sm),var(--glass-in)}
  .pcard-back{background:
      repeating-linear-gradient(45deg,transparent 0 7px,color-mix(in srgb,var(--brand) 14%,transparent) 7px 14px),
      color-mix(in srgb,var(--panel2) 92%,var(--brand));
    color:color-mix(in srgb,var(--brand) 70%,var(--text));font-size:1.5rem;font-weight:800}
  .pcard-front{transform:rotateY(180deg);font-size:2rem;font-weight:800;letter-spacing:.02em;line-height:1;
    background:color-mix(in srgb,var(--panel) 96%,var(--brand));color:var(--text)}
  /* pickable: the three face-down cards in pick-one-of-three */
  .pcard.pick{cursor:pointer}
  .pcard.pick:hover,.pcard.pick:focus-visible{transform:translateY(-10px) rotate(-2deg)}
  .pcard.pick:focus-visible{outline:2px solid var(--brand);outline-offset:4px;border-radius:13px}
  .pcard.gone{transform:translateY(14px) scale(.82);opacity:0;filter:blur(2px);
    transition:transform .45s var(--ease),opacity .45s ease,filter .45s ease}
  .pcard.deal{animation:cardDeal .45s var(--ease) both}
  @keyframes cardDeal{from{transform:translateY(-26px) rotate(-8deg);opacity:0}to{transform:none;opacity:1}}
  /* the rarest card earns a glow rather than another line of text */
  .pcard.gold .pcard-front{border-color:var(--amber);
    background:color-mix(in srgb,var(--amber) 12%,var(--panel));
    box-shadow:0 0 0 1px var(--amber),0 0 26px color-mix(in srgb,var(--amber) 45%,transparent)}
  .pcard.gold{animation:cardGlow 1.6s ease-in-out .6s 2}
  @keyframes cardGlow{50%{filter:brightness(1.22)}}
  .pcard.ref .pcard-face{border-style:dashed}
  .pcard-tag{position:absolute;left:50%;bottom:-19px;transform:translateX(-50%);
    font-size:.625rem;font-weight:700;color:var(--muted);white-space:nowrap}

  /* ---- mode picker + table ---- */
  .card-modes{display:flex;justify-content:center;margin-bottom:var(--sp-3)}
  .card-panel{border:1px solid var(--line);border-radius:14px;padding:var(--sp-4);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);box-shadow:var(--glass-in)}
  .card-hint{text-align:center;color:var(--muted);font-size:.75rem;line-height:1.5;margin-bottom:8px}
  /* drawn from the bootstrap, waiting on the one fact only the server has */
  .card-panel.card-hold .card-acts,.card-panel.card-hold .deck{opacity:.5}
  .card-panel.card-hold .card-modes{opacity:.75}
  .card-acts{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin-top:14px}
  .card-result{text-align:center;padding:10px 12px;border-radius:12px;margin-top:12px;
    background:color-mix(in srgb,var(--green) 9%,transparent);
    border:1px solid color-mix(in srgb,var(--green) 26%,transparent);
    animation:capIn .32s var(--ease) both}
  .card-result.miss{background:color-mix(in srgb,var(--amber) 9%,transparent);
    border-color:color-mix(in srgb,var(--amber) 26%,transparent)}
  .card-result .rt{font-weight:700;font-size:.9375rem}
  .card-result .rx{font-size:1.375rem;font-weight:800;color:var(--green);
    font-variant-numeric:tabular-nums;animation:expNumUp .8s var(--ease)}
  .card-result.miss .rx{color:var(--amber)}
  .card-result .rb{font-size:.6875rem;color:var(--muted);margin-top:2px}
  /* the week rail: five days, and what the fifth one is worth */
  .card-week{display:flex;align-items:center;gap:8px;margin-top:12px;
    font-size:.6875rem;color:var(--muted)}
  .card-week .pips{display:flex;gap:4px}
  .card-week .pips i{width:9px;height:9px;border-radius:50%;
    background:color-mix(in srgb,var(--line-strong) 60%,transparent)}
  .card-week .pips i.on{background:var(--brand);
    box-shadow:0 0 0 2px color-mix(in srgb,var(--brand) 26%,transparent)}
  .card-week .pips i.done{background:var(--green);box-shadow:0 0 0 2px color-mix(in srgb,var(--green) 26%,transparent)}
  .card-week b{color:var(--text);font-variant-numeric:tabular-nums}
  .card-hist{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
  .card-hist span{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:20px;
    font-size:.625rem;border:1px solid var(--line);color:var(--muted);
    background:color-mix(in srgb,var(--panel2) 60%,transparent);font-variant-numeric:tabular-nums}
  .card-hist span b{color:var(--text);font-weight:800;margin-right:2px}
  .card-hist span.win{border-color:color-mix(in srgb,var(--green) 40%,transparent);color:var(--green)}
  @media (prefers-reduced-motion:reduce){
    .pcard,.pcard-in,.pcard.gone{transition:none}
    .pcard.deal,.pcard.gold,.badge-item.fresh,.badge-item.fresh::after,.card-result{animation:none}
  }
  @media (max-width:520px){
    .pcard{width:70px;height:100px}
    .pcard-front{font-size:2.1rem}
    .badge-grid{grid-template-columns:repeat(auto-fill,minmax(66px,1fr))}
  }
  .exp-num.up{animation:expNumUp .6s var(--ease)}
  @keyframes expNumUp{0%,100%{color:var(--text)}30%{color:var(--green)}}
  /* "+N" rises from just below the capsule and is absorbed into it.
     It used to float UPWARD, from top:-2px to translateY(-34px) - but this
     badge sits in a sticky header pinned to top:0 with only 12px of padding
     above it, which puts the badge's own top edge at y=15px. The pop therefore
     crossed y=0 about a quarter of the way in and spent the rest of its 1.1s
     above the top edge of the window, where there is nothing to see and no way
     to scroll to it. Anchoring it under the badge keeps every frame on screen,
     and travelling toward the capsule instead of away from it is what actually
     reads as the EXP landing there.

     IT IS A PILL, NOT BARE TEXT, and it hangs off .who rather than off the
     badge. Both of those were why it may as well not have existed: eleven
     pixels of green text on a text-shadow, travelling into whatever happens to
     sit under the header — a warning banner, a pale card — is invisible over
     half of them; and the badge is a <button> whose innerHTML updateExpBadge()
     rewrites, so a second award in the same breath (finish a task after 8pm and
     it pays +20 and then +3) deleted the first pop mid-flight. It also spent
     0.9s total, of which only about a third was at full opacity.
     A <button> is a poor parent for an absolutely positioned child in any case:
     Safari has historically clipped them to the button box whatever overflow
     says, which would hide this on an iPhone and nowhere else. */
  .exp-pop{position:absolute;z-index:30;pointer-events:none;white-space:nowrap;
    display:inline-flex;align-items:center;gap:3px;padding:2px 10px;border-radius:20px;
    font-size:.875rem;font-weight:800;line-height:1.4;font-variant-numeric:tabular-nums;
    color:var(--green);background:color-mix(in srgb,var(--green) 16%,var(--panel));
    border:1px solid color-mix(in srgb,var(--green) 45%,transparent);
    box-shadow:0 6px 18px color-mix(in srgb,var(--green) 26%,transparent),var(--shadow-sm);
    transform:translate(-50%,14px);animation:expPop 1.5s var(--ease) forwards}
  @keyframes expPop{
    0%{opacity:0;transform:translate(-50%,16px) scale(.7)}
    14%{opacity:1;transform:translate(-50%,5px) scale(1.16)}
    26%{opacity:1;transform:translate(-50%,8px) scale(1)}
    74%{opacity:1;transform:translate(-50%,6px) scale(1)}
    100%{opacity:0;transform:translate(-50%,-6px) scale(.72)}
  }
  /* The travel is decoration; the number is not. A reader who asked the OS for
     less movement still gets told what they earned — it just stands still. */
  @media (prefers-reduced-motion:reduce){
    .exp-pop{animation:expPopHold 1.5s linear forwards}
    @keyframes expPopHold{
      0%{opacity:0;transform:translate(-50%,8px)}
      6%{opacity:1;transform:translate(-50%,8px)}
      80%{opacity:1;transform:translate(-50%,8px)}
      100%{opacity:0;transform:translate(-50%,8px)}
    }
  }
  [data-theme="light"] .exp-lv{color:color-mix(in srgb,var(--lv,var(--text)) 55%,var(--text))}
  :root:not([data-theme="light"]) .exp-lv{color:color-mix(in srgb,var(--lv,var(--text)) 76%,var(--text))}
  /* ---- LEADERBOARD ---- */
  /* THE SHEET HAS TO FIT THE WINDOW.
     Five departments, a level card and a footnote came to 808px of modal, and a
     laptop viewport is 735 — the bottom of the list and the close button were
     simply off the screen, with the page behind it scrolling instead. The rows
     are the part that grows, so the rows are the part that scrolls; the title,
     the tabs and the footer stay where they are. */
  #lbBody{max-height:min(66vh,620px);overflow-y:auto;overscroll-behavior:contain;
    padding-right:4px;margin-right:-4px}
  .lb-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
  .lb-tab{padding:5px 12px;border-radius:20px;font-size:.75rem;font-weight:600;cursor:pointer;
    background:var(--panel2);border:1px solid var(--line);color:var(--muted);font-family:inherit;
    transition:border-color .15s ease,color .15s ease,background .15s ease}
  .lb-tab:hover{border-color:var(--brand);color:var(--text)}
  .lb-tab.on{background:color-mix(in srgb,var(--brand) 12%,transparent);border-color:var(--brand);color:var(--brand2)}
  .lb-list{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
  .lb-you{font-style:normal;font-size:.6875rem;color:var(--brand2);font-weight:600}
  /* WHAT THE WHOLE TEAM ADDS UP TO, above the rows: five numbers with nothing
     to measure them against is a table, not an overview. */
  .lb-sum{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
    padding:0 2px 10px;border-bottom:1px solid var(--line);margin:0 0 10px}
  @media (max-width:879px){ .lb-sum{margin-top:12px} }
  .lb-sum-n{font-size:.9375rem;font-weight:800;color:var(--text)}
  .lb-sum-sub{font-size:.75rem;color:var(--muted)}
  /* The two readings, one press apart, on the same line as the total they
     re-cut. */
  .lb-by{display:flex;gap:2px;margin-left:auto;padding:2px;border-radius:9px;
    background:color-mix(in srgb,var(--panel2) 70%,transparent);border:1px solid var(--line)}
  .lb-bybtn{font-family:inherit;font-size:.6875rem;font-weight:700;cursor:pointer;
    padding:3px 9px;border-radius:7px;border:0;background:transparent;color:var(--muted);
    transition:background var(--dur-fast) ease,color var(--dur-fast) ease}
  .lb-bybtn:hover{color:var(--text)}
  .lb-bybtn.on{background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand2)}
  /* The bar is the comparison: the gaps between teams become something you can
     see instead of arithmetic you have to do. */
  .lb-bar{display:block;height:4px;border-radius:2px;margin:6px 0 5px;
    background:color-mix(in srgb,var(--line) 55%,transparent);overflow:hidden}
  .lb-bar i{display:block;height:100%;border-radius:2px;
    background:linear-gradient(90deg,var(--brand),var(--brand2));transition:width .4s var(--ease)}
  .lb-row.me .lb-bar i{background:linear-gradient(90deg,var(--brand2),var(--brand))}
  .lb-top{display:flex;align-items:baseline;gap:8px}
  .lb-top .lb-name{flex:1;min-width:0}
  /* A leaderboard with no positions on it was just a sorted list. Three medals,
     then plain numerals — enough to read the shape at a glance. */
  .lb-rank.r1{color:#f0b429}
  .lb-rank.r2{color:#b8c1cc}
  .lb-rank.r3{color:#c2803f}
  .lb-rank{color:var(--muted)}
  /* How far the next level is, in points rather than in per cent — a
     percentage is not something anybody can act on. */
  .lb-togo{display:flex;align-items:center;gap:5px;margin-top:7px;
    font-size:.75rem;font-weight:700;color:var(--brand2)}
  .lb-togo .ic{width:13px;height:13px}
  .lb-row{display:flex;align-items:center;gap:12px;padding:9px 13px 10px;border-radius:10px;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);border:1px solid var(--line);
    transition:border-color .18s ease,transform .12s ease}
  .lb-row:hover{border-color:var(--brand);transform:translateY(-1px)}
  .lb-row.me{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 8%,var(--panel2))}
  .lb-rank{font-size:1rem;font-weight:800;min-width:18px;text-align:center;
    font-variant-numeric:tabular-nums}
  .lb-avatar{font-size:1.1rem;flex:none}
  .lb-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
  .lb-name{font-weight:600;font-size:.875rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .lb-lv{font-size:.6875rem;font-weight:700;color:var(--muted)}
  .lb-exp{font-size:.8125rem;font-weight:700;color:var(--brand2);white-space:nowrap;font-variant-numeric:tabular-nums}
  .lb-note{display:flex;gap:6px;align-items:flex-start;margin-top:12px;line-height:1.45;
    font-size:.6875rem;line-height:1.5;color:var(--muted)}
  .lb-note .ic{width:12px;height:12px;flex:none;margin-top:2px}
  .lb-me{padding:var(--sp-3) var(--sp-4);border-radius:12px;border:1px solid var(--brand);
    background:color-mix(in srgb,var(--brand) 6%,var(--panel))}
  .lb-me-lv{font-size:1rem;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:7px}
  .lb-me-lv .ic{width:18px;height:18px;flex:none}
  .lb-me-exp{font-size:1.25rem;font-weight:800;color:var(--text);margin-bottom:8px}
  .lb-progress{display:flex;align-items:center;gap:8px}
  .lb-progress-bar{flex:1;height:6px;border-radius:3px;background:color-mix(in srgb,var(--line) 55%,transparent);overflow:hidden}
  .lb-progress-bar div{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--brand),var(--brand2));transition:width .4s var(--ease)}
  .lb-next{font-size:.75rem;color:var(--muted);white-space:nowrap}
  .lb-max{font-size:.875rem;color:var(--amber);font-weight:700;display:flex;align-items:center;gap:6px}
  .select-pop .sel-opt:hover,.select-pop .sel-opt.hi{background:color-mix(in srgb,var(--brand) 13%,transparent)}
  .select-pop .sel-opt[aria-selected="true"]{font-weight:600}

  /* ---- DATE BUTTON (opens the themed calendar popover) ----
     Started as a toolbar-only control; also used inside the task form now, so
     the rule isn't scoped under .toolbar any more — same look everywhere. */
  .datebtn{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    color:var(--text);border:1px solid var(--line-strong);box-shadow:var(--glass-drop);
    border-radius:8px;padding:0 10px;min-height:var(--tap);font-family:inherit;font-size:.875rem;
    display:inline-flex;align-items:center;gap:7px;cursor:pointer;font-variant-numeric:tabular-nums;
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease,background-color var(--dur-fast) ease}
  .datebtn:hover{border-color:var(--brand)}
  .datebtn:focus-visible{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .datebtn .ic{color:var(--brand2)}
  /* inside a .field it sits where a full-width input would, left-aligned like one */
  .field .datebtn{width:100%;justify-content:flex-start}

  /* ---- THEMED DATE PICKER POPOVER ---- */
  /* anchored under the date button; reuses .pop glass + .on show; narrower than palette.
     z-index above .modal (50), same reasoning as .select-pop: it now opens from the
     task form's date button too, so it must float above the modal it lives in. */
  .pop.pick-pop{position:fixed;min-width:296px;width:296px;padding:12px;border-radius:16px;z-index:55}
  .pick-head{display:flex;align-items:center;gap:6px;margin-bottom:8px}
  .pick-title{flex:1;text-align:center;font-weight:700;font-size:.9375rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .pnav{width:30px;height:30px;border-radius:8px;border:1px solid var(--line);background:transparent;color:var(--muted);
    display:grid;place-items:center;cursor:pointer;flex:none;
    transition:color var(--dur-fast) ease,border-color var(--dur-fast) ease,background-color var(--dur-fast) ease}
  .pnav:hover{color:var(--text);border-color:var(--brand)}
  .pnav .ic{width:16px;height:16px}
  .pick-dow{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px}
  .pick-dow span{text-align:center;font-size:.6875rem;font-weight:600;color:var(--muted);padding:4px 0;text-transform:uppercase}
  .pick-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
  .pick-day{position:relative;aspect-ratio:1;border-radius:9px;border:1px solid transparent;background:transparent;
    color:var(--text);font-family:inherit;font-size:.8125rem;font-weight:600;cursor:pointer;display:flex;
    align-items:center;justify-content:center;min-width:0;
    transition:background-color var(--dur-fast) ease,color var(--dur-fast) ease,border-color var(--dur-fast) ease,transform 120ms ease}
  .pick-day:hover:not(.pad){background:color-mix(in srgb,var(--brand) 15%,transparent);border-color:var(--brand)}
  .pick-day:active{transform:scale(.92)}
  .pick-day.pad{color:transparent;cursor:default}
  .pick-day.pad:hover{background:transparent}
  .pick-day.today{border-color:var(--brand)}
  .pick-day.sel{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
  .pick-day.sel:hover{background:var(--brand)}
  .pick-day.holiday{color:var(--red)}
  .pick-foot{display:flex;justify-content:flex-end;margin-top:8px;padding-top:8px;border-top:1px solid var(--line)}
  .pick-foot .btn{padding:0 12px;min-height:30px}
  /* a busy dot under days that have tasks (filled from the fetched month) */
  .pick-day .bdot{position:absolute;left:50%;bottom:3px;transform:translateX(-50%);width:4px;height:4px;border-radius:50%;background:var(--brand2)}

  h2.section{font-size:1.125rem;margin:22px 0 4px;line-height:1.35}
  .sub{color:var(--muted);font-size:.8125rem;margin-bottom:16px}
  .card{background:var(--panel);background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent);
    -webkit-backdrop-filter:blur(16px) saturate(1.5);backdrop-filter:blur(16px) saturate(1.5);
    border:1px solid var(--line);border-radius:14px;padding:16px;box-shadow:var(--glass-in),var(--glass-drop)}
  .row{display:flex;gap:var(--sp-3);flex-wrap:wrap}
  .toolbar{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center;margin:14px 0}
  .toolbar input,.toolbar select{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    color:var(--text);border:1px solid var(--line-strong);box-shadow:var(--glass-drop);
    border-radius:8px;padding:0 10px;min-height:var(--tap);font-family:inherit;font-size:.875rem;
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .toolbar input:focus,.toolbar select:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring);outline:none}
  .listfilter{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:2px 0 10px}
  /* A magnifier inside the field. This row is four or five pale rounded boxes
     in a line, and which one you type INTO was left to the placeholder to
     explain — the first thing to be cut off as the row narrows. The flex sizing
     moves to the wrapper so the field still takes up the slack, and the icon
     takes no pointer events so a click at the left edge still lands in the
     input. */
  .listfilter .lf-find{position:relative;display:flex;align-items:center;flex:1;min-width:140px}
  .listfilter .lf-find>.ic{position:absolute;left:11px;width:15px;height:15px;
    color:var(--muted);pointer-events:none;transition:color var(--dur-fast) ease}
  .listfilter .lf-find:focus-within>.ic{color:var(--brand2)}
  .listfilter input[type=search]{width:100%;min-width:0;background:var(--panel2);
    border:1px solid var(--line);border-radius:9px;padding:7px 11px 7px 33px;font:inherit;color:var(--text);outline:none}
  .listfilter input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .listfilter .roletabs{margin:0}
  .listfilter .roletabs button{padding:6px 11px;font-size:.75rem}
  /* ---- quick-add bar: inline task creation at the top of the day list ----
     THE BUG THIS LAYOUT REPLACES: .qadd was a flex ROW and .qadd-opts was one of
     its children, so opening the options row put category, priority and "paste
     list" on the same line as the input — which was squeezed to 57px on a
     laptop and 22px on a phone, i.e. the field you type into disappeared the
     moment you asked for the options next to it. A column with one row inside
     it cannot do that. */
  /* MINIMAL AT REST. The bar IS the field — one border, not a bordered box with
     a second bordered input inside it — and everything else on the row earns its
     place or is not drawn: the hint is empty unless a shortcut is useful right
     now, the submit arrow appears with the first character, and the category
     chip only when it is not the obvious one. Resting state is a placeholder
     and a single settings button. */
  .qadd{display:flex;flex-direction:column;margin:0 0 10px;
    padding:4px 5px 4px 12px;border-radius:11px;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    border:1px solid var(--line);transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .qadd:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .qadd.open{padding-bottom:8px;padding-left:10px;padding-right:10px}
  .qadd-main{display:flex;gap:6px;align-items:center;min-width:0}
  .qadd input{flex:1;border:none;background:transparent;
    padding:7px 0;font:inherit;color:var(--text);outline:none;min-width:0}
  .qadd input::placeholder{color:var(--muted)}
  .qadd-main button{height:28px;border:none;border-radius:8px;flex:none;
    background:transparent;color:var(--muted);cursor:pointer;display:inline-flex;
    align-items:center;justify-content:center;gap:5px;padding:0 6px;
    transition:background var(--dur-fast) ease,color var(--dur-fast) ease,opacity var(--dur-fast) ease}
  .qadd-main button .ic{width:15px;height:15px}
  .qadd-main button:hover{background:color-mix(in srgb,var(--line) 55%,transparent);color:var(--text)}
  .qadd-main button:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
  /* the one shortcut worth naming right now — empty, and therefore absent, the
     rest of the time */
  .qadd-kbd{flex:none;font:inherit;font-size:.5625rem;line-height:1;white-space:nowrap;
    color:var(--muted);padding:3px 5px;border-radius:4px;
    background:color-mix(in srgb,var(--line) 50%,transparent);opacity:.8;
    transition:opacity var(--dur-fast) ease,color var(--dur-fast) ease}
  .qadd-kbd:empty{display:none}
  .qadd-kbd.ready{color:var(--brand2);opacity:1;
    background:color-mix(in srgb,var(--brand) 16%,transparent)}
  /* submit — appears with the first character. There used to be no way at all
     to file a task with a mouse, or on a phone without hunting for the
     keyboard's own Go key. */
  .qadd-go{width:28px;padding:0}
  .qadd-go:disabled{width:0;padding:0;opacity:0;pointer-events:none;overflow:hidden}
  .qadd-go:not(:disabled){color:var(--brand2);
    background:color-mix(in srgb,var(--brand) 16%,transparent)}
  .qadd-go:not(:disabled):hover{background:color-mix(in srgb,var(--brand) 28%,transparent)}
  /* the settings button grows a label only when the choice is not the obvious
     one, so at rest the whole bar is a placeholder and one small icon */
  .qadd-opt{max-width:46%;font-size:.6875rem}
  .qadd-opt .qadd-pick{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
  .qadd-opt .qadd-pick.hide{display:none}
  .qadd-opt .qadd-pick i{font-style:normal;opacity:.45;margin:0 3px}
  .qadd.open .qadd-opt{color:var(--brand2);background:color-mix(in srgb,var(--brand) 12%,transparent)}
  @media(max-width:560px){ .qadd-opt .qadd-pick{display:none} .qadd-opt{max-width:none} }
  /* expanded row: its own line, always */
  .qadd-opts{display:none;gap:8px;align-items:center;margin-top:8px;
    padding-top:8px;border-top:1px solid var(--line);flex-wrap:wrap}
  .qadd.open .qadd-opts{display:flex;animation:fadeIn var(--dur-fast) ease both}
  .qadd-opts label{font-size:.75rem;color:var(--muted);white-space:nowrap}
  .qadd-opts select{background:var(--panel2);border:1px solid var(--line);border-radius:8px;
    padding:4px 8px;font:inherit;font-size:.8125rem;color:var(--text);min-height:30px}
  .qadd-paste-btn{font-size:.75rem;color:var(--muted);white-space:nowrap;padding:4px 8px;
    margin-left:auto;display:inline-flex;align-items:center;gap:5px;
    border:1px dashed var(--line);border-radius:8px;background:transparent;cursor:pointer;
    transition:border-color var(--dur-fast) ease,color var(--dur-fast) ease}
  .qadd-paste-btn .ic{width:14px;height:14px}
  .qadd-paste-btn:hover{border-color:var(--brand);color:var(--brand2)}
  /* ---- daily summary card ----
     "1 จาก 3" is a ratio, and a ratio set in small grey text is a number nobody
     converts into a sense of how the day is going. The bar does that; the
     figures stay underneath it for whoever wants them. */
  .day-sum{margin-top:14px;padding:11px 14px 10px;border-radius:12px;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    border:1px solid var(--line);font-size:.8125rem}
  .day-sum-bar{height:5px;border-radius:99px;overflow:hidden;margin-bottom:9px;
    background:color-mix(in srgb,var(--line-strong) 55%,transparent)}
  .day-sum-bar i{display:block;height:100%;border-radius:99px;background:var(--green);
    transition:width var(--dur) var(--ease)}
  .day-sum.all{border-color:color-mix(in srgb,var(--green) 40%,var(--line));
    background:color-mix(in srgb,var(--green) 8%,transparent)}
  .day-sum-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
  .day-sum-i{display:flex;align-items:center;gap:5px;color:var(--muted);white-space:nowrap}
  .day-sum-i .ic{width:14px;height:14px;flex:none}
  .day-sum-i b{color:var(--text);font-variant-numeric:tabular-nums}
  .day-sum-i.done b{color:var(--green)}
  .day-sum-i.done .ic{color:var(--green)}
  .day-sum-i.left b{color:var(--text)}
  .day-sum-i.exp b{color:var(--amber)}
  .day-sum-i.streak b{color:var(--brand2)}
  @media(prefers-reduced-motion:reduce){ .day-sum-bar i{transition:none} }
  /* ---- bulk create modal ----
     A count told you four rows were coming and never what they would say. The
     preview is how you catch that a "- " marker was stripped, that a wrapped
     line became two tasks, or that you pasted the wrong block entirely. */
  .bulk-create textarea{width:100%;min-height:150px;border:2px solid var(--line);border-radius:10px;
    padding:10px 12px;font:inherit;font-size:.875rem;color:var(--text);
    background:var(--panel2);resize:vertical;outline:none;
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .bulk-create textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .bulk-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
  .bulk-row label{font-size:.75rem;color:var(--muted);white-space:nowrap}
  .bulk-row select{background:var(--panel2);border:1px solid var(--line);border-radius:8px;
    padding:4px 8px;font:inherit;font-size:.8125rem;color:var(--text);min-height:30px}
  .bulk-kbd{margin-left:auto;font:inherit;font-size:.625rem;line-height:1;color:var(--muted);
    padding:3px 6px;border-radius:5px;border:1px solid var(--line);background:var(--panel2);
    opacity:.75;white-space:nowrap}
  .bulk-prev{margin-top:12px}
  .bulk-count{font-size:.8125rem;font-weight:700;color:var(--brand2);margin-bottom:6px}
  .bulk-count.over{color:var(--red)}
  .bulk-empty{font-size:.8125rem;color:var(--muted)}
  /* No scroll and no cap here: the list is already cut to BULK_PREVIEW items
     with a count of the rest underneath, and a max-height on top of that sliced
     the last visible row in half — which reads as "something is broken" rather
     than "there is more". */
  .bulk-list{margin:0;padding:0 0 0 1.35em}
  .bulk-list li{font-size:.8125rem;color:var(--text);padding:2px 0;word-break:break-word}
  .bulk-list li::marker{color:var(--muted);font-variant-numeric:tabular-nums}
  .bulk-more{margin-top:5px;font-size:.75rem;color:var(--muted)}
  /* the day list and the team schedule share this control */
  .sortbtn{flex:none;gap:5px;padding:0 11px;font-size:.75rem;min-height:var(--tap)}
  .sortbtn.on{border-color:var(--brand);color:var(--brand2);
    background:color-mix(in srgb,var(--brand) 11%,transparent)}
  .sortbtn .ic{width:14px;height:14px}
  /* items-per-page dropdown: the same themed-select button as elsewhere, just
     sized to sit beside the sort button instead of filling a form row */
  .pagesizebtn{width:auto;min-height:var(--tap);padding:0 10px;font-size:.75rem;gap:6px}
  .listfilter .no-match{text-align:center;width:100%;padding:10px 0;color:var(--muted);font-size:.8125rem}
  /* day-list pager: mirrors the date picker's own prev/next (.pnav) so paging
     through a long day reads as the same control language as everywhere else */
  .listpager{display:flex;align-items:center;justify-content:space-between;gap:10px;
    margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
  .lp-info{font-size:.8125rem;color:var(--muted)}
  .lp-nav{display:flex;align-items:center;gap:8px}
  .lp-page{font-size:.8125rem;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
  .pnav:disabled{opacity:.4;cursor:not-allowed}
  .pnav:disabled:hover{color:var(--muted);border-color:var(--line)}
  /* a hairline between status runs, so a group reads as a block rather than as
     rows that merely happen to be adjacent */
  .task.grp-top{margin-top:var(--sp-3);position:relative}
  .task.grp-top::before{content:"";position:absolute;left:10%;right:10%;top:calc(-1 * var(--sp-2));
    height:1px;background:linear-gradient(90deg,transparent,var(--line),transparent)}
  .task.grp-top:first-child{margin-top:0}
  .task.grp-top:first-child::before{display:none}
  /* "เลือกทั้งหมด" sits in the filter row, not the batch bar, so filtering down to
     ต้องทำ and taking the whole result is two clicks instead of tick-then-select */
  .listfilter .bulk-all{display:inline-flex;align-items:center;gap:6px;flex:none;
    padding:0 10px 0 8px;border:1px solid var(--line);border-radius:9px;
    background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    font-size:.75rem;font-weight:600;color:var(--muted);cursor:pointer;min-height:var(--tap);
    white-space:nowrap;transition:border-color var(--dur-fast) ease,color var(--dur-fast) ease}
  .listfilter .bulk-all:hover{border-color:var(--line-strong);color:var(--text)}
  .listfilter .bulk-all:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  /* the wrapper already shows focus; a second ring inside it just looks doubled */
  .listfilter .bulk-all .chk:focus-visible{box-shadow:var(--glass-in)}
  .listfilter .bulk-all.on{border-color:var(--brand);color:var(--brand2);
    background:color-mix(in srgb,var(--brand) 11%,transparent)}
  .listfilter .bulk-all .chk{width:17px;height:17px;border-radius:5px}
  .btn{background:var(--brand);color:var(--on-brand);border:1px solid transparent;border-radius:9px;
    padding:0 16px;min-height:var(--tap);font-weight:600;display:inline-flex;align-items:center;gap:6px;
    cursor:pointer;font-size:.8125rem;font-family:inherit;position:relative;white-space:nowrap;
    transition:filter var(--dur-fast) ease,box-shadow var(--dur-fast) ease,transform 120ms ease}
  .btn:hover:not(:disabled){filter:brightness(1.06);box-shadow:var(--shadow-sm)}
  .btn:active:not(:disabled){transform:scale(.97)}   /* transform only — never reflows neighbours */
  .btn:disabled{opacity:.55;cursor:not-allowed;filter:grayscale(.35)}
  .btn.ghost.del{color:var(--muted)}
  .btn.ghost.del:hover,.btn.ghost.del:focus-visible{color:var(--red);border-color:var(--red)}
  .btn.ghost{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    border-color:var(--line-strong);color:var(--text);box-shadow:var(--glass-drop)}
  .btn.ghost:hover:not(:disabled){border-color:var(--brand);background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent)}
  .btn.sm{padding:0 10px;font-size:.75rem;min-height:var(--tap)}
  /* A class sets display on both of these, and a class outranks the UA
     stylesheet's [hidden]{display:none} — so hiding one by attribute did
     nothing at all. The announcement strip and two badges already carry this
     line for the same reason; anything that hides itself by attribute needs it. */
  .btn[hidden],.iconbtn[hidden]{display:none}
  /* Icon-only variant: same footprint as .btn.sm but square, no label — used
     where a row repeats several actions and a text caption on every one of
     them adds width without adding clarity (the icon + title/aria-label do). */
  .btn.sm.icon{padding:0;width:var(--tap);min-width:var(--tap);justify-content:center;gap:0}
  .btn.busy{color:transparent!important;pointer-events:none}
  .btn.busy>.ic{visibility:hidden}
  .btn.busy::after{content:"";position:absolute;inset:0;margin:auto;width:15px;height:15px;border-radius:50%;
    border:2px solid rgba(128,128,128,.45);border-top-color:var(--on-brand);
    animation:spin .6s linear infinite}
  @supports (color:color-mix(in srgb,red,blue)){
    .btn.busy::after{border-color:color-mix(in srgb,var(--on-brand) 35%,transparent);border-top-color:var(--on-brand)}
  }
  .btn.ghost.busy::after{border-color:var(--line-strong);border-top-color:var(--brand)}
  .cutoff{font-size:.75rem;color:var(--amber);display:inline-flex;align-items:center;gap:5px}

  .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
  .kpi{background:var(--panel);background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent);
    -webkit-backdrop-filter:blur(16px) saturate(1.5);backdrop-filter:blur(16px) saturate(1.5);
    border:1px solid var(--line);border-radius:14px;padding:16px;box-shadow:var(--glass-in),var(--glass-drop);
    /* background/color follow the page's theme cross-fade (var(--dur), like .task) */
    transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease,
    background-color var(--dur) ease,color var(--dur) ease}
  .kpi:hover{transform:translateY(-2px);border-color:var(--brand);box-shadow:var(--shadow-sm),var(--glass-in),var(--glass-drop)}
  .kpi .n{font-size:1.75rem;font-weight:700;line-height:1.15;font-variant-numeric:tabular-nums}
  .kpi .l{color:var(--muted);font-size:.75rem;margin-top:2px}
  .kpi .th{color:var(--muted);font-size:.75rem}
  /* A bare number does not say whether it is good. The icon labels the metric at
     a glance and the sub-line gives it the denominator it needs to mean anything. */
  .kpi .kh{display:flex;align-items:center;gap:6px;margin-bottom:7px}
  .kpi .kh .ic{width:15px;height:15px;flex:none}
  .kpi .kh .th{font-weight:600;letter-spacing:.1px}
  .kpi .sub{color:var(--muted);font-size:.6875rem;margin-top:5px;line-height:1.4}
  .kpi .meter{height:5px;border-radius:3px;margin-top:9px;overflow:hidden;
    background:color-mix(in srgb,var(--line) 55%,transparent)}
  .kpi .meter i{display:block;height:100%;border-radius:3px;transition:width .6s var(--ease)}
  /* A KPI that drills down is a real <button>, so it is reachable by keyboard
     and announced as actionable — the div version stays for the ones that
     aren't (the per-person drill sheet, the skeleton). */
  button.kpi{display:block;width:100%;text-align:left;font:inherit;color:inherit;cursor:pointer}
  button.kpi .kh{position:relative;padding-right:16px}
  /* The affordance has to survive a hover-less touch screen, so it is always
     drawn — hover only slides it. */
  button.kpi .kmore{position:absolute;right:0;top:50%;margin-top:-7px;width:14px;height:14px;
    color:var(--muted);opacity:.65;transition:transform .18s ease,opacity .18s ease}
  button.kpi:hover .kmore,button.kpi:focus-visible .kmore{opacity:1;transform:translateX(2px)}
  /* KPI drill-down list: rows are the same .task surface used everywhere else,
     with a compact header line naming what the list is counting. */
  .kd-sub{color:var(--muted);font-size:.8125rem;margin:2px 0 12px}
  /* Paging a scrolled-away pager is guesswork, so the sheet is a column and
     only the list moves — title, subtitle and pager keep their place. */
  #kpiModal .sheet{display:flex;flex-direction:column;max-height:88vh}
  #kpiModal .sheet>:not(#kpiDrillBody){flex:none}
  #kpiDrillBody{flex:1 1 auto;min-height:0;overflow:auto;
    margin:0 -4px;padding:0 4px}   /* room for a row's focus ring at the edges */
  .kd-list{display:flex;flex-direction:column;gap:8px}
  .kd-empty{color:var(--muted);font-size:.875rem;text-align:center;padding:26px 10px}
  .kd-who{display:inline-flex;align-items:center;gap:5px;color:var(--brand2);font-weight:600;
    font-size:.75rem}
  .kd-who .ic{width:13px;height:13px;flex:none}
  .kd-num{display:flex;align-items:center;gap:8px;flex:none}
  .kd-num b{font-variant-numeric:tabular-nums;font-size:.8125rem;min-width:34px;text-align:right}
  .kd-list .task.clickable{cursor:pointer}
  .kd-list .task.clickable:hover{border-color:var(--brand)}
  /* One stacked bar for the whole team: where the period's work actually sits.
     Colour alone never carries it — every segment is also named in the legend. */
  /* DISC cell: the leading letter carries the identity, the split bar carries
     the shape. Both in one compact cell so a table of ten people still reads. */
  .disc-chip{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
  .disc-chip b{font-size:.8125rem;font-weight:800;letter-spacing:.3px}
  .disc-chip b span{font-weight:600;color:var(--muted);font-size:.6875rem}
  .disc-mini{display:flex;width:74px;height:9px;border-radius:5px;overflow:hidden;
    background:color-mix(in srgb,var(--line) 55%,transparent)}
  .disc-mini i{display:block;height:100%}
  .disc-mini i+i{box-shadow:inset 1px 0 0 color-mix(in srgb,var(--panel) 60%,transparent)}
  /* Generation chip: a quiet outline, not a colour-coded badge. Age band is
     context for how to brief someone, and a loud tint next to a completion
     rate would read as a verdict on the person. */
  .gen-chip{display:inline-flex;align-items:center;gap:5px;padding:1px 9px;border-radius:20px;
    font-size:.75rem;font-weight:700;white-space:nowrap;
    border:1px solid var(--line-strong);color:var(--text);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent)}
  .gen-chip .gc{font-size:.6875rem;font-weight:600;color:var(--muted)}
  .disc-tag{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:20px;
    font-size:.6875rem;font-weight:700;border:1px solid var(--line);
    background:color-mix(in srgb,var(--green) 12%,transparent);color:var(--green)}
  .disc-tag.inf{background:color-mix(in srgb,var(--muted) 12%,transparent);color:var(--muted)}
  .mixbar{margin:14px 0 4px}
  .mixbar .mb-t{display:flex;align-items:center;gap:6px;font-size:.6875rem;font-weight:700;
    letter-spacing:.4px;text-transform:uppercase;color:var(--muted);margin-bottom:7px}
  .mixbar .mb-t .ic{width:13px;height:13px;color:var(--brand)}
  .mixbar .mb-bar{display:flex;height:11px;border-radius:6px;overflow:hidden;
    background:color-mix(in srgb,var(--line) 45%,transparent);box-shadow:var(--glass-in)}
  .mixbar .mb-bar i{display:block;height:100%;transition:width .6s var(--ease)}
  .mixbar .mb-bar i+i{box-shadow:inset 1px 0 0 color-mix(in srgb,var(--bg) 55%,transparent)}
  .mixbar .mb-key{display:flex;flex-wrap:wrap;gap:5px 14px;margin-top:9px}
  .mixbar .mb-key span{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;color:var(--muted)}
  .mixbar .mb-key b{color:var(--text);font-variant-numeric:tabular-nums}
  .mixbar .mb-key i{width:9px;height:9px;border-radius:3px;flex:none}
  .mixbar .mb-empty{font-size:.8125rem;color:var(--muted);padding:6px 0}
  /* Skeletons echo the real cell shapes, so nothing jumps when the data lands. */
  .sk-cell-name{height:15px;width:60%;border-radius:5px}
  .sk-cell-num{height:15px;width:34px;border-radius:5px;margin:0 auto}
  .sk-cell-bar{height:10px;width:78%;border-radius:5px}
  .sk-cell-btn{height:26px;width:62px;border-radius:8px;margin:0 auto}
  .sk-avatar{width:22px;height:22px;border-radius:50%;flex:none}
  /* A placeholder sized in em sits inside the real text box, so the line height
     that governs the final layout also governs the skeleton — the difference
     between "roughly the same size" and "does not move at all". */
  .sk-txt{display:inline-block;height:1em;border-radius:5px;vertical-align:-.1em}

  /* Wide tables scroll inside their own box instead of breaking the page. */
  .tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
  table{width:100%;border-collapse:collapse;min-width:560px}
  th,td{text-align:left;padding:11px 10px;border-bottom:1px solid var(--line);font-size:.875rem}
  th{color:var(--muted);font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.4px}
  tr.clickable{cursor:pointer;transition:background-color var(--dur-fast) ease}
  tr.clickable:hover{background:color-mix(in srgb,var(--panel2) 55%,transparent)}
  .bar{height:8px;background:var(--panel2);border-radius:6px;overflow:hidden;min-width:90px;
    border:1px solid var(--line)}
  .bar>i{display:block;height:100%;transition:width .6s cubic-bezier(.22,.9,.28,1)}
  .pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:20px;font-size:.75rem;font-weight:600;border:1px solid var(--line);background:var(--panel2);color:var(--text);cursor:default;transition:transform .12s ease,box-shadow .22s ease,border-color .22s ease,background-color .22s ease}
  .pill:hover{transform:translateY(-1px)}
  .pill:active{transform:translateY(0);transition-duration:.04s}
  .pill .dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--sw-accent);flex:none;transform:scale(.6);transition:transform .34s cubic-bezier(.2,.8,.2,1),opacity .22s ease,background-color .22s ease;opacity:0}
  .pill .dot.on{transform:scale(1);opacity:1;box-shadow:0 0 0 2px var(--ring)}
  .pill.p-plan{background:var(--s-plan-bg);color:var(--s-plan-fg);--sw-accent:var(--s-plan-fg)}
  .pill.p-prog{background:var(--s-prog-bg);color:var(--s-prog-fg);--sw-accent:var(--s-prog-fg)}
  .pill.p-done{background:var(--s-done-bg);color:var(--s-done-fg);--sw-accent:var(--s-done-fg)}
  .pill.p-carry{background:var(--s-carry-bg);color:var(--s-carry-fg);--sw-accent:var(--s-carry-fg)}
  .pill.p-leave{background:var(--s-leave-bg);color:var(--s-leave-fg);--sw-accent:var(--s-leave-fg)}
  .cat{display:inline-block;padding:2px 8px;border-radius:6px;font-size:.75rem;background:var(--panel2);color:var(--muted)}
  .prio{font-weight:700;font-size:.75rem;display:inline-flex;align-items:center;gap:3px}
  .prio.H{color:var(--red)}.prio.M{color:var(--amber)}.prio.L{color:var(--muted)}

  .task{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3);border:1px solid var(--line);
    border-radius:11px;margin-bottom:var(--sp-2);background:var(--panel2);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(12px) saturate(1.4);backdrop-filter:blur(12px) saturate(1.4);
    box-shadow:var(--glass-in),var(--glass-drop);
    /* background/color follow the page's own cross-fade (var(--dur), same as body):
       without it the translucent row snaps to the new theme while the page behind
       it is still blending, which reads as colour bands split row by row */
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease,opacity var(--dur) ease,
    background-color var(--dur) ease,color var(--dur) ease}
  /* Light: form fields and controls take the lighter --ctl surface. */
  [data-theme="light"] .btn.ghost,
  [data-theme="light"] .iconbtn,
  [data-theme="light"] .selbtn,
  [data-theme="light"] .toolbar .datebtn,
  [data-theme="light"] .toolbar input,
  [data-theme="light"] .toolbar select,
  [data-theme="light"] .listfilter input,
  [data-theme="light"] .field input,
  [data-theme="light"] .field select,
  [data-theme="light"] .field textarea{
    background:color-mix(in srgb,var(--ctl) var(--glass-a),transparent)}
  .task:hover{border-color:var(--brand);box-shadow:var(--shadow-sm),var(--glass-in),var(--glass-drop)}
  /* Light: a row is a card, the same white surface as .card/.kpi/.cal-cell, so
     the list stops reading as a stack of beige bars on a beige page. */
  [data-theme="light"] .task{background:color-mix(in srgb,var(--panel) var(--glass-a),transparent)}
  .task[onclick]{cursor:pointer}
  .task.saving{opacity:.55;pointer-events:none}
  /* multi-select: the box sits outside .grow so a long title never squeezes it,
     and it swallows its own clicks so ticking a row does not also open it */
  /* a comfortable hit area around a box that stays visually small */
  .task .tsel{flex:none;display:grid;place-items:center;width:30px;height:30px;margin:-4px;cursor:pointer}
  /* until a row is picked the box is quiet, so a list of unticked rows does not
     read as a wall of empty squares — it comes up on hover, focus or selection */
  .task .tsel .chk{opacity:.5;transition:opacity var(--dur-fast) ease,
    background-color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease),transform 110ms ease}
  .task:hover .tsel .chk,.task .tsel .chk:hover,.task .tsel .chk:focus-visible,
  .task .tsel .chk:checked,.task.picked .tsel .chk{opacity:1}
  .task.picked{border-color:var(--brand);
    background:color-mix(in srgb,var(--brand) 9%,var(--panel2));
    box-shadow:0 0 0 1px color-mix(in srgb,var(--brand) 30%,transparent),var(--glass-in)}
  /* the batch bar replaces the filter row while a selection is live, so the two
     never compete for the same strip of space */
  .bulkbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:2px 0 10px;
    padding:7px 11px;border-radius:11px;
    border:1px solid color-mix(in srgb,var(--brand) 34%,transparent);
    background:color-mix(in srgb,var(--brand) 11%,var(--panel2));
    animation:capIn .2s var(--ease) both}
  .bulkbar.hide{display:none}
  .bulkbar .bulk-n{font-weight:700;font-size:.8125rem;color:var(--brand2)}
  .bulkbar .bulk-act{margin-left:auto;display:flex;gap:6px;flex-wrap:wrap}
  /* pulse-dot, shown once per row per browser + user: green = never seen here,
     amber = seen before but the content changed since. Both are non-text marks,
     so they only need the 3:1 bar against --panel2, which every palette clears. */
  .task .fresh-dot{width:8px;height:8px;border-radius:50%;background:var(--green);
    box-shadow:0 0 0 3px var(--ring);flex:none;animation:fresh-pop .45s ease-out}
  .task .fresh-dot.upd{background:var(--amber)}
  @keyframes fresh-pop{from{transform:scale(.3);opacity:.2}to{transform:scale(1);opacity:1}}
  /* What a background refresh just changed, said in the text itself: the value
     lights up for a moment and settles back to whatever colour it already had.
     Only a `from` keyframe is declared, so the implicit `to` is the element's
     own computed colour — one rule works for a green KPI, a red one, or plain
     body text, and nothing has to be restored afterwards. Animations outrank
     inline styles in the cascade, so the KPI numbers (which carry an inline
     colour) flash too. The global prefers-reduced-motion rule collapses this
     to nothing, which is the right answer: the value is still correct. */
  @keyframes fx-flash{from{color:var(--brand2)}}
  @keyframes fx-flash-bg{from{background:color-mix(in srgb,var(--brand) 22%,transparent)}}
  .fx-flash{animation:fx-flash 1.4s var(--ease) both}
  .fx-flash-bg{animation:fx-flash-bg 1.4s var(--ease) both}
  .task .title{font-weight:600;font-size:.9375rem;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  .task .meta{color:var(--muted);font-size:.8125rem;margin-top:3px;display:flex;gap:var(--sp-2);
    flex-wrap:wrap;align-items:center}
  .task .meta>span{display:inline-flex;align-items:center;gap:4px}
  .task .meta .pill{flex:none}
  /* Progress at a glance: five pips that fill as the task advances. A bare "40%"
     told you nothing you could read while scanning a day; the exact figure now
     lives in the meter's tooltip and the row's aria-label. */
  .task .meta .prog{display:inline-flex;align-items:center;gap:2px}
  .task .meta .prog i{width:8px;height:5px;border-radius:2px;flex:none;
    background:color-mix(in srgb,var(--line-strong) 70%,transparent);
    transition:background-color var(--dur) ease}
  .task .meta .prog i.f{background:var(--brand)}
  .task .meta .prog.full i.f{background:var(--green-solid)}
  /* Form progress preview: slightly larger pips for the task modal */
  #formProgressPreview{display:flex;align-items:center;gap:6px}
  #formProgressPreview .prog{display:inline-flex;align-items:center;gap:3px}
  #formProgressPreview .prog i{width:10px;height:6px;border-radius:3px;flex:none;
    background:color-mix(in srgb,var(--line-strong) 55%,transparent);
    transition:background-color var(--dur) ease}
  #formProgressPreview .prog i.f{background:var(--brand)}
  #formProgressPreview .prog.full i.f{background:var(--green-solid)}
  .task .meta .ats{color:var(--brand2);font-weight:600;display:inline-flex;align-items:center;gap:3px}
  /* 20, not 16. A face is how a row is scanned — you find your own meeting by
     recognising a disc, not by reading a name — and at 16px an emoji avatar is
     a smudge and an initial is unreadable at a glance. The row is 44px tall
     either way, so the four extra pixels cost nothing but were never taken. */
  .task .meta .ats .avatar.sm{width:20px;height:20px;font-size:.625rem}
  .task .meta .ats .avatar.sm.pic{font-size:12px}
  .task .meta .ats .nick{color:var(--brand2);opacity:.8;font-weight:600}
  /* ---- one person, named the same way everywhere ----
     Face, official name, then the nickname people actually use. Sized to sit
     in a row's meta line beside .cat and .ats; .person.lg is the table-sized
     variant. Anything that used to print a bare staffName goes through
     personHtml() so a face and a nickname appear in every list. */
  .person{display:inline-flex;align-items:center;gap:4px;min-width:0;max-width:100%}
  .person .avatar.sm{width:20px;height:20px;font-size:.625rem}
  .person .avatar.sm.pic{font-size:12px}
  .person .pname{font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .person .pnick{color:var(--brand2);font-weight:600;white-space:nowrap;flex:none}
  .person .pnick::before{content:'·';margin-right:3px;color:var(--muted);opacity:.6}
  .person.lg{gap:6px}
  .person.lg .avatar.sm{width:26px;height:26px;font-size:.6875rem}
  .person.lg .avatar.sm.pic{font-size:15px}
  .person.lg .pname{font-weight:700}
  .person.lg .pnick{font-size:.8125rem}
  .task .right{margin-left:auto;display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;justify-content:flex-end}
  .grow{flex:1;min-width:0}
  /* task status control: the same rounded status "pill" shown everywhere else
     (list rows, task view) — .pill + statusPillCls() give it shape and color,
     these rules just make that pill a real, tappable <select> instead of a
     boxy dropdown, so it stays keyboard- and mobile-picker-native */
  .statsel.pill{cursor:pointer;font-family:inherit;
    border:1px solid color-mix(in srgb,currentColor 45%,transparent);   /* was transparent — too easy to miss as a control */
    min-height:var(--tap);padding:3px 22px 3px 12px;
    transition:transform .12s ease,box-shadow .22s ease,border-color .22s ease,background-color .22s ease}
  .statsel.pill:hover{transform:translateY(-1px)}
  .statsel.pill:active{transform:scale(.96)}
  .statsel.pill:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring);outline:none}
  .empty{color:var(--muted);text-align:center;padding:26px;border:1px dashed var(--line-strong);border-radius:12px;
    display:flex;flex-direction:column;align-items:center;gap:var(--sp-2);font-size:.875rem}
  .empty .ic{color:var(--muted);opacity:.7}

  /* status pills in task meta align vertically with dates/categories */
  .task .meta .pill{align-self:center}

  /* ============ MODALS ============ */
  .modal{position:fixed;inset:0;background:var(--overlay);display:none;place-items:center;z-index:50;padding:16px;
    -webkit-backdrop-filter:blur(3px) saturate(1.05);backdrop-filter:blur(3px) saturate(1.05)}
  .modal.on{display:grid;animation:fadeIn .18s ease both}
  .modal.off{display:grid;animation:fadeOut .16s ease both;pointer-events:none}
  .sheet{background:var(--panel);background:color-mix(in srgb,var(--panel) var(--glass-a3),transparent);
    -webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);
    border:1px solid var(--line);border-radius:16px;width:100%;max-width:640px;
    max-height:90vh;overflow:auto;padding:20px;box-shadow:var(--shadow),var(--glass-in)}
  .modal.on .sheet{animation:popIn .22s cubic-bezier(.2,.9,.3,1.2) both}
  .modal.off .sheet{animation:popOut .16s ease both}

  /* มีอะไรใหม่ / What's New — modal with two read modes: a sliding feature
     deck (default) and a plain long list. Panels are absolutely stacked, so
     each is a screen-sized "slide"; .wn-off slides + fades the inactive one. */
  /* Wider than every other sheet because the rail takes a column of its own.
     At 720px the deck would have paid for the navigation beside it — every
     stage is a mockup drawn against roughly 660px of width, and they would all
     have reflowed to make room. */
  #whatsnewModal .sheet{max-width:880px;max-height:95vh;display:flex;flex-direction:column}
  .wn-top{display:flex;align-items:center;gap:8px}
  .wn-top .modal-title{margin:0;flex:1;display:flex;align-items:center;gap:8px}
  .wn-vers{font-size:.6875rem;color:var(--muted);font-weight:600;border:1px solid var(--line);
    border-radius:20px;padding:2px 9px;white-space:nowrap}
  /* ---- the release rail ----
     One release at a time, chosen from a dated list down the left. Both
     releases ran end to end before, which made "what shipped today" and "what
     did I miss in the last one" the same twenty-four-slide walk — and the only
     way to tell where one ended and the next began was the little date chip on
     each slide. */
  .wn-main{display:flex;gap:var(--sp-4);flex:1;min-height:0;margin-top:14px}
  .wn-col{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0}
  .wn-rail{flex:none;width:150px;display:flex;flex-direction:column;gap:var(--sp-2);
    padding-right:var(--sp-3);border-right:1px solid var(--line);overflow:auto}
  .wn-rail-h{font-size:.625rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
    color:var(--muted);padding:0 var(--sp-1) var(--sp-1)}
  .wn-relb{display:flex;flex-direction:column;align-items:flex-start;gap:3px;width:100%;
    padding:var(--sp-2) var(--sp-3);border-radius:10px;border:1px solid transparent;
    background:transparent;cursor:pointer;text-align:start;
    transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
  .wn-relb:hover{background:var(--panel2)}
  .wn-relb .d{font-size:.8125rem;font-weight:700;color:var(--text);line-height:1.35;
    font-variant-numeric:tabular-nums}
  .wn-relb .n{font-size:.6875rem;color:var(--muted);line-height:1.4}
  .wn-relb.on{background:color-mix(in srgb,var(--brand) 12%,transparent);
    border-color:color-mix(in srgb,var(--brand) 30%,transparent)}
  .wn-relb.on .d{color:var(--brand2)}
  .wn-body{position:relative;overflow:hidden;flex:1;min-height:420px}
  .wn-slide{position:absolute;inset:0;overflow:auto;padding:2px 4px 6px;display:flex;flex-direction:column;gap:12px;
    transition:opacity .25s ease,transform .25s ease}
  .wn-slide.wn-off{opacity:0;transform:translateX(-14px);pointer-events:none}
  /* Centre a feature slide's content in the window instead of pinning it to the
     top: most stages are shorter than the 420px body, which left a dead gap
     underneath. Auto margins rather than justify-content:center - a centred
     flex column clips its own overflow at the top when the content does not
     fit, whereas autos simply resolve to 0 and it scrolls normally. Scoped to
     the two children a feature slide has, so the hero and ending slides keep
     the centring they already do for themselves. */
  .wn-slide>.wn-fhead{margin-top:auto}
  .wn-slide>[data-wn-i]{margin-bottom:auto}
  .wn-slide>.wn-fhead{align-items:center;text-align:center}
  .wn-lede{color:var(--muted);font-size:.875rem}
  /* Intro hero slide: centred icon, bold title, subtitle, feature pills */
  .wn-hero{display:flex;flex-direction:column;align-items:center;
    justify-content:center;justify-content:safe center;
    text-align:center;gap:13px;min-height:100%;padding:8px 12px}
  .wn-hero h2{font-size:1.25rem;font-weight:700;margin:0;line-height:1.35}
  .wn-hero p{color:var(--muted);font-size:.875rem;margin:0;max-width:420px;line-height:1.5}
  /* The intro used to list every feature as a pill. At a dozen entries that was
     a wall of text that pushed the hero icon off the top and clipped its own
     last row — and it front-loaded the whole deck, leaving the slides that
     follow with nothing to reveal. It now shows the three headline changes and
     says how many more are behind the next button. */
  .wn-count{display:inline-flex;align-items:center;gap:7px;padding:5px 14px;border-radius:16px;
    font-size:.8125rem;font-weight:700;letter-spacing:.2px;
    background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand2);
    border:1px solid color-mix(in srgb,var(--brand) 28%,transparent)}
  .wn-count .ic{width:15px;height:15px}
  .wn-his{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;width:100%;max-width:520px;margin-top:2px}
  .wn-hi{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;
    padding:14px 10px;border-radius:13px;border:1px solid var(--line);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    box-shadow:var(--glass-in);
    transition:transform .18s var(--ease),border-color .18s ease,box-shadow .18s ease}
  .wn-hi:hover{transform:translateY(-2px);border-color:var(--brand);box-shadow:var(--shadow-sm),var(--glass-in)}
  .wn-hi .bx{width:34px;height:34px;border-radius:11px;display:grid;place-items:center;flex:none;
    background:color-mix(in srgb,var(--brand) 15%,transparent);
    border:1px solid color-mix(in srgb,var(--brand) 26%,transparent)}
  .wn-hi .bx .ic{width:17px;height:17px;color:var(--brand)}
  .wn-hi .tx{font-size:.75rem;font-weight:600;line-height:1.4}
  /* "ใหม่" on the intro card for whatever this release actually added. The
     deck reappears for everyone on a version bump, so without a mark the three
     cards all read as equally new when two of them shipped months ago. Driven
     by nw:1 in WN_ALL rather than hard-coded to one slide, so next release it
     moves by editing the table. Filled brand, not an outline: it has to win
     against the card it sits on. */
  .wn-hi .wn-new{position:absolute;top:6px;right:6px;padding:1px 7px;border-radius:20px;
    font-size:.5625rem;font-weight:800;letter-spacing:.06em;line-height:1.7;
    color:var(--on-brand);background:var(--brand);box-shadow:var(--shadow-sm)}
  .wn-more{color:var(--muted);font-size:.75rem;display:inline-flex;align-items:center;gap:6px}
  .wn-more .ic{width:13px;height:13px}
  /* Below the sheet's own width the rail cannot have a column of its own
     without taking it from the deck, so it lies down across the top instead —
     still the same list, still showing which release you are reading. */
  @media (max-width:760px){
    .wn-main{flex-direction:column;gap:var(--sp-3);margin-top:12px}
    .wn-rail{width:auto;flex-direction:row;overflow-x:auto;padding:0 0 var(--sp-2);
      border-right:0;border-bottom:1px solid var(--line)}
    .wn-rail-h{display:none}
    .wn-relb{flex:none;width:auto;padding:var(--sp-1) var(--sp-3)}
  }
  @media (max-width:560px){
    /* stacked rows read better narrow, and the hero sheds the parts that only
       decorate so all three still fit above the fold */
    .wn-his{grid-template-columns:1fr;max-width:320px;gap:7px}
    .wn-hi{flex-direction:row;text-align:left;padding:9px 12px;gap:11px}
    /* a row card puts its title where the badge sits — keep them apart */
    .wn-hi.nw{padding-right:46px}
    .wn-hi .bx{width:28px;height:28px;border-radius:9px}
    .wn-hi .bx .ic{width:15px;height:15px}
    .wn-hero{gap:9px;padding:10px 10px}
    .wn-hero p{font-size:.8125rem}
  }
  /* where the board slide's strip was, once the reader has put it away: the
     same 38px the strip occupied, so dismissing it does not move the page */
  .wn-board-empty{height:38px;border-radius:10px;border:1px dashed var(--line-strong);opacity:.45}
  .wn-board-note{display:flex;align-items:flex-start;gap:var(--sp-2);margin-top:var(--sp-3);
    color:var(--muted);font-size:.75rem;line-height:1.55}
  .wn-board-note .ic{width:13px;height:13px;flex:none;margin-top:2px;color:var(--brand)}
  /* ---- version history: one dated block per release ----
     The deck is for meeting a feature; this is for answering "what changed
     since the 11th", which is a different question and wants a different
     shape — dated, scannable, and complete rather than one thing at a time. */
  .wn-notes{padding:2px 2px 8px}
  .wn-rel{margin-bottom:26px}
  .wn-rel:last-child{margin-bottom:6px}
  .wn-rel-h{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
    padding-bottom:9px;border-bottom:1px solid var(--line)}
  .wn-rel-h b{font-size:1.0625rem;font-weight:700;line-height:1.3}
  .wn-rel-h .d{color:var(--muted);font-size:.75rem;font-variant-numeric:tabular-nums}
  .wn-rel-h .n{margin-left:auto;flex:none;padding:2px 10px;border-radius:20px;
    font-size:.6875rem;font-weight:700;color:var(--brand2);
    background:color-mix(in srgb,var(--brand) 12%,transparent);
    border:1px solid color-mix(in srgb,var(--brand) 26%,transparent)}
  .wn-rel.now .wn-rel-h b{color:var(--brand2)}
  .wn-rel-l{color:var(--muted);font-size:.8125rem;line-height:1.6;margin:10px 0 12px}
  /* the release a slide belongs to, next to its number */
  .wn-ver{flex:none;padding:1px 8px;border-radius:20px;font-size:.625rem;font-weight:700;
    letter-spacing:.02em;font-variant-numeric:tabular-nums;color:var(--muted);
    border:1px solid var(--line-strong)}
  .wn-ver.now{color:var(--brand2);border-color:color-mix(in srgb,var(--brand) 40%,transparent);
    background:color-mix(in srgb,var(--brand) 10%,transparent)}
  /* Feature slide layout: numbered badge + title + description + stage */
  .wn-fhead{display:flex;flex-direction:column;gap:8px}
  .wn-fnum{display:inline-flex;align-items:center;justify-content:center;
    width:22px;height:22px;border-radius:7px;font-size:.6875rem;font-weight:700;
    background:color-mix(in srgb,var(--brand) 15%,transparent);color:var(--brand);
    border:1px solid color-mix(in srgb,var(--brand) 25%,transparent);flex:none}
  .wn-ftitle{display:flex;align-items:center;gap:7px;font-size:.9375rem;font-weight:700}
  .wn-ftitle svg{width:18px;height:18px;color:var(--brand);flex:none}
  .wn-fdesc{color:var(--muted);font-size:.8125rem;line-height:1.5}
  /* Ending slide hero */
  .wn-end{display:flex;flex-direction:column;align-items:center;justify-content:center;
    text-align:center;gap:14px;height:100%;padding:20px 12px}
  .wn-end-icon{width:56px;height:56px;border-radius:16px;display:flex;align-items:center;justify-content:center;
    background:linear-gradient(135deg,color-mix(in srgb,var(--green) 18%,transparent),color-mix(in srgb,var(--green) 8%,transparent));
    border:1px solid color-mix(in srgb,var(--green) 25%,transparent)}
  .wn-end-icon svg{width:28px;height:28px;color:var(--green);fill:none;stroke:currentColor;
    stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
  .wn-end h2{font-size:1.125rem;font-weight:700;margin:0}
  .wn-end p{color:var(--muted);font-size:.875rem;margin:0;max-width:400px;line-height:1.5}
  /* Live feature stages: each slide embeds the app's REAL controls — the same
     swatches, load capsule, bell rows and view tabs the user presses daily —
     so the tour demonstrates the feature and lets you try it on the spot.
     Stages are rebuilt on slide entry; wnShow() starts/stops their loops. */
  .wn-stage{border:1px solid var(--line);border-radius:12px;background:var(--panel2);
    padding:14px;display:flex;flex-direction:column;gap:10px;margin-top:4px;min-height:150px;
    align-items:center;justify-content:center}
  .wn-stage .roletabs{padding:3px}
  .wn-stage .roletabs button{padding:7px 13px;font-size:.75rem}
  /* Slide 1 shows the colour picker itself rather than a shrunken imitation of
     it: the same .pop shell, the same .pop-head with its light/dark toggle, and
     the same three-column grid of full-size swatches scrolling inside the pop
     the way the live popover does. Only two things have to change - the pop is
     unpinned from the viewport, and it is capped shorter so it sits inside a
     slide. The old override squeezed the grid to six columns of 26px chips at
     9px type, which is what stopped it looking like the real control. */
  .wn-pop{position:static;top:auto;right:auto;left:auto;bottom:auto;z-index:auto;
    display:block;width:100%;min-width:0;max-width:340px;max-height:258px;margin:0 auto;
    animation:none;box-shadow:var(--shadow-sm),var(--glass-in)}
  .wn-stage-pop{padding:8px;align-items:stretch}
  /* .wn-stage .sw would outrank the base .sw rules, so the real sizing is
     restated here at a specificity that wins inside the pop */
  .wn-stage .wn-pop .swatches{grid-template-columns:repeat(3,1fr);gap:10px;width:auto}
  .wn-stage .wn-pop .sw{padding:6px 4px 8px;gap:7px;font-size:.6875rem;border-radius:14px;
    cursor:pointer;border:none;background:transparent;color:var(--muted);
    transition:transform .12s ease,color .12s ease}
  .wn-stage .wn-pop .sw:hover{transform:translateY(-1px);color:var(--text)}
  .wn-stage .wn-pop .sw:active{transform:translateY(0);transition-duration:.04s}
  .wn-stage .wn-pop .sw[aria-pressed="true"]{color:var(--text);font-weight:600}
  .wn-stage .wn-pop .sw .dot{height:38px;border-radius:12px;
    transition:transform .34s cubic-bezier(.2,.8,.2,1),border-color .22s ease,box-shadow .22s ease}
  .wn-stage .wn-pop .sw:active .dot{transform:scale(.94)}
  .wn-stage .wn-pop .sw .dot::after{width:15px;height:15px;left:7px}
  .wn-stage .wn-pop .sw.pressed .dot{animation:wnSwatchPop .34s cubic-bezier(.2,.8,.2,1) both;
    box-shadow:0 0 0 2px var(--ring)}
  /* profile stage: a real, clickable avatar grid — makes it obvious avatars are
     pickable instead of only showing a static initials circle */
  .wn-ava-opt{width:34px;height:34px;border-radius:10px;font-size:1.05rem;cursor:pointer;
    display:flex;align-items:center;justify-content:center;background:var(--panel);
    border:1px solid var(--line);transition:transform .12s ease,border-color .12s ease,box-shadow .22s ease}
  .wn-ava-opt:hover{transform:translateY(-2px);border-color:var(--line-strong)}
  .wn-ava-opt:active{transform:translateY(0);transition-duration:.04s}
  .wn-ava-opt.on{border-color:var(--brand);box-shadow:0 0 0 2px var(--ring)}
  @keyframes wnSwatchPop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.18);opacity:1}100%{transform:scale(1)}}
  /* no pointer-events:auto here — a hidden slide (.wn-off) sets pointer-events:none
     and children forcing auto would intercept clicks meant for the visible slide */
  /* the slide is narrower than the report modal: full axis names wrap to two
     lines there and push the caveat off the bottom, so this stage uses initials */
  .wn-stage .disc-row{grid-template-columns:20px 1fr 28px;gap:9px;margin-bottom:7px}
  .wn-stage .ai-note{font-size:.625rem;padding:7px 9px}
  .wn-stage .ai-head h3{font-size:.875rem}
  .wn-stage .bell-row{background:var(--panel2);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    border:1px solid var(--line);border-radius:10px;width:100%}
  .wn-stage .bsec-t{align-self:flex-start}
  .wn-frame{border:1px solid var(--line);border-radius:10px;background:var(--panel);
    padding:8px;width:min(300px,100%);display:flex;flex-direction:column;gap:6px;
    transition:width .6s var(--ease);margin:0 auto;position:relative;overflow:hidden}
  .wn-frame.wide{width:min(430px,100%)}
  .wn-frame .task{margin:0;padding:8px 10px;gap:8px}
  .wn-frame .task .title{font-size:.75rem}
  .wn-frame .task .meta{font-size:.625rem;margin-top:2px;gap:6px}
  .wn-frame .task .right{display:none}
  /* one quiet line under a stage, for the thing the picture cannot say */
  .wn-note{font-size:.6875rem;color:var(--muted);text-align:center;line-height:1.6}
  .wn-caret{opacity:.7;animation:wnCaret 1s steps(1) infinite}
  @keyframes wnCaret{50%{opacity:0}}
  @media (prefers-reduced-motion:reduce){ .wn-caret{animation:none} }
  /* a release that went out between planned ones wears it on the rail, so
     "did I miss a feature drop, or a repair?" is answered before you click */
  .wn-hot{font-style:normal;font-size:.5625rem;font-weight:800;letter-spacing:.02em;
    margin-left:6px;padding:2px 6px;border-radius:99px;vertical-align:middle;
    color:var(--amber);background:color-mix(in srgb,var(--amber) 16%,transparent)}
  .wn-fade{animation:wnFade .35s ease both}
  @keyframes wnFade{from{opacity:0;transform:translateY(5px)}to{opacity:1}}
  @media (prefers-reduced-motion:reduce){
    .wn-fade{animation:none}
    .wn-frame{transition:none}
  }
  .wn-foot{display:flex;align-items:center;gap:8px;border-top:1px solid var(--line);padding-top:12px;margin-top:12px}
  /* Loop runner: play/pause auto-advance button with timer ring */
  .wn-loop{position:relative;width:28px;height:28px;flex:none}
  .wn-loop button{width:28px;height:28px;border-radius:50%;border:1.5px solid var(--line);
    background:var(--panel2);cursor:pointer;display:flex;align-items:center;justify-content:center;
    padding:0;transition:border-color .2s ease,background .2s ease}
  .wn-loop button:hover{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 10%,var(--panel2))}
  .wn-loop button svg{width:14px;height:14px;color:var(--muted);transition:color .2s ease}
  .wn-loop button:hover svg{color:var(--brand)}
  .wn-loop.on button{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,var(--panel2))}
  .wn-loop.on button svg{color:var(--brand)}
  .wn-loop-ring{position:absolute;inset:-3px;border-radius:50%;pointer-events:none}
  .wn-loop-ring circle{fill:none;stroke:var(--brand);stroke-width:2;stroke-linecap:round;
    stroke-dasharray:94.25;stroke-dashoffset:94.25;transform:rotate(-90deg);transform-origin:center;
    transition:none}
  .wn-loop.on .wn-loop-ring circle{transition:stroke-dashoffset .1s linear}
  .wn-dots{flex:1;height:8px;min-width:120px;border-radius:4px;
    background:color-mix(in srgb,var(--panel2) 60%,var(--bg));
    overflow:hidden;cursor:pointer;position:relative}
  /* visual track is 8px; ::before extends the clickable area to the 24px WCAG floor */
  .wn-dots::before{content:'';position:absolute;inset:-8px -4px}
  .wn-dots button{height:100%;border:none;padding:0;cursor:pointer;width:0;top:0;left:0;
    background:linear-gradient(90deg,color-mix(in srgb,var(--brand) 85%,#fff),var(--brand2));
    transition:width .45s cubic-bezier(.4,0,.2,1);border-radius:0 4px 4px 0;pointer-events:none;position:absolute;
    box-shadow:inset 0 -1px 1px rgba(0,0,0,.2)}
  .wn-dots button.on{width:var(--wn-prog,0%)}
  .wn-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
  .wn-list li{display:flex;gap:9px;align-items:flex-start;font-size:.875rem}
  .wn-list li svg{flex:none;margin-top:2px;color:var(--green)}
  .wn-list li span{color:var(--muted)}
  .field{margin-bottom:var(--sp-3)}
  .field label{display:block;color:var(--muted);font-size:.8125rem;margin-bottom:5px;font-weight:500}
  .field input,.field select,.field textarea{width:100%;background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    color:var(--text);border:1px solid var(--line-strong);border-radius:9px;padding:9px 11px;min-height:var(--tap);
    font-size:.875rem;font-family:inherit;box-shadow:var(--glass-drop);
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .field textarea{min-height:64px;resize:vertical}
  .field input:focus,.field select:focus,.field textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring);outline:none}
  .g2{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3)}
  .g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:var(--sp-3)}
  /* the add/edit task form used to run one field per row even for short
     dropdowns — compacted to grid rows plus a touch less vertical rhythm so
     the whole thing fits with less scrolling and reads as one glance instead
     of a long questionnaire */
  #taskFormFields .field{margin-bottom:10px}
  #taskFormFields .g3{margin-bottom:10px}
  /* progress is derived from status, not its own decision — a small caption
     under the status field reads faster than a whole separate labelled row */
  #formProgressField{margin-top:-2px}
  #formProgressField label{display:none}
  #formProgressField #formProgressPreview{display:flex;align-items:center;gap:6px}
  /* read-only view panel inside the task modal */
  #taskViewPanel{display:block}
  #taskViewPanel .vp-wrap{border-top:1px solid var(--line);padding-top:14px;margin-top:4px}
  #taskViewPanel .vp-title{font-size:1.0625rem;font-weight:700;color:var(--text);margin-bottom:4px;word-break:break-word;line-height:1.35}
  #taskViewPanel .vp-meta{color:var(--muted);font-size:.8125rem;margin-bottom:6px;padding-bottom:10px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:6px}
  #taskViewPanel .vp-meta .ic{color:var(--brand2)}
  #taskViewPanel .vp-row{display:grid;grid-template-columns:120px 1fr;gap:6px 12px;align-items:baseline;margin-bottom:11px}
  #taskViewPanel .vp-row .vp-label{color:var(--muted);font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;opacity:.95}
  #taskViewPanel .vp-row .vp-value{color:var(--text);font-size:.9375rem;word-break:break-word;line-height:1.45}
  #taskViewPanel .vp-row .vp-value.empty{color:var(--muted);font-style:normal}
  /* A link found inside a note (see linkify). Underlined as well as coloured —
     in a panel that is otherwise plain sentences, colour alone does not say
     "pressable", and it is the only cue for a reader who cannot separate the
     two. Allowed to break mid-address so one long URL cannot widen the panel
     past the phone it is being read on. */
  .lnk{color:var(--brand2);text-decoration:underline;text-underline-offset:2px;word-break:break-all}
  .lnk:hover{color:var(--brand)}
  .lnk:focus-visible{outline:2px solid var(--ring);outline-offset:2px;border-radius:3px}
  /* several people in one value cell: wrap as chips instead of one long line */
  #taskViewPanel .vp-value .vp-people{display:flex;flex-wrap:wrap;gap:4px 12px}
  #taskViewPanel .vp-row .vp-value .pill{margin-top:2px}
  #taskViewPanel .vp-row .vp-value .pill .dot{display:inline-block;width:12px;height:12px;border-radius:50%;background:var(--sw-accent);flex:none;transform:scale(.55);transition:transform .34s cubic-bezier(.2,.8,.2,1),opacity .22s ease,background-color .22s ease;opacity:0}
  #taskViewPanel .vp-row .vp-value .pill .dot.on{transform:scale(1);opacity:1;box-shadow:0 0 0 2px var(--ring)}
  #taskViewPanel .vp-title-ish{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:14px}
  #taskViewPanel .vp-badge{font-size:.7rem;font-weight:600;padding:3px 9px;border-radius:20px;border:1px solid var(--line);background:var(--panel2);color:var(--text);letter-spacing:.02em}
  #taskViewPanel .vp-row .pill{margin-top:1px}
  [data-theme="light"] #taskViewPanel .vp-border{border-color:var(--line)}

  /* ถึง / ผู้ที่เกี่ยวข้อง — mention picker (chips + searchable dropdown) */
  /* .mwrap alone (position:relative, no z-index) never opens its own stacking
     context, so .mlist's z-index:40 was being evaluated against the *parent*
     context instead — later fields in the form (the native date input in
     particular) could still paint over it. Giving the field itself a z-index
     opens that context here, so .mlist reliably wins regardless of what
     follows it in the form. */
  #mentionField{position:relative;z-index:5}
  .mwrap{position:relative;display:flex;flex-wrap:wrap;gap:5px;align-items:center;cursor:text;
    background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    border:1px solid var(--line-strong);border-radius:9px;padding:4px 6px;min-height:var(--tap);
    box-shadow:var(--glass-drop);transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease}
  .mwrap:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .mwrap input{flex:1;min-width:110px;border:0!important;background:transparent!important;box-shadow:none!important;
    padding:3px 4px;min-height:0;font-size:.875rem;color:var(--text)}
  .mwrap input:disabled{display:none}
  .m-chip{display:inline-flex;align-items:center;gap:5px;padding:1px 4px;border-radius:16px;font-size:.8125rem;
    background:color-mix(in srgb,var(--brand) 13%,var(--panel2));
    border:1px solid color-mix(in srgb,var(--brand) 32%,transparent);color:var(--text);white-space:nowrap}
  .m-chip b{font-weight:600;color:var(--brand2)}
  .m-chip .avatar.sm{width:22px;height:22px;font-size:.625rem}
  .m-chip .avatar.sm.pic{font-size:13px}
  .m-chip .nick{color:var(--muted);font-size:.75rem}
  .m-chip button{background:none;border:0;color:var(--muted);cursor:pointer;width:17px;height:17px;line-height:1;
    display:inline-flex;align-items:center;justify-content:center;border-radius:50%;font-size:.8rem;padding:0}
  .m-chip button:hover{background:color-mix(in srgb,var(--red) 30%,transparent);color:var(--red)}
  .mlist{position:absolute;left:-1px;right:-1px;top:calc(100% + 3px);z-index:40;max-height:230px;overflow:auto;
    background:var(--panel);background:color-mix(in srgb,var(--panel) 96%,transparent);
    -webkit-backdrop-filter:blur(20px) saturate(1.5);backdrop-filter:blur(20px) saturate(1.5);
    border:1px solid var(--line-strong);border-radius:11px;box-shadow:var(--shadow);padding:4px}
  .mlist .mitem{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;cursor:pointer;font-size:.875rem}
  .mlist .mitem:hover,.mlist .mitem.cur{background:color-mix(in srgb,var(--brand) 13%,transparent)}
  .mlist .mitem .at{color:var(--brand2);font-weight:700;flex:none}
  /* The name column takes the row's free space (flex:1) so every name starts
     at the same left edge right after the avatar, and margin-left:0 keeps it
     there even if a bare .who rule elsewhere carries an auto margin. The
     department tail keeps its own width at the far end. */
  .mlist .mitem .who{display:flex;align-items:baseline;gap:5px;min-width:0;flex:1;margin-left:0}
  .mlist .mitem .who b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .mlist .mitem .nick{color:var(--brand2);font-size:.8125rem;white-space:nowrap;flex:none}
  .mlist .mitem small{color:var(--muted);margin-left:8px;font-size:.75rem;flex:none}
  /* the slice of text the query actually matched — so a hit on a nickname or a
     department reads as deliberate rather than looking like a stray result */
  .mlist .mitem .hl{background:color-mix(in srgb,var(--brand) 28%,transparent);
    color:var(--text);border-radius:3px;padding:0 1px;font-weight:700}
  .mlist .mnone{color:var(--muted);font-size:.8125rem;padding:8px 10px}

  .modal-title{font-size:1.0625rem;font-weight:700;margin-bottom:14px;line-height:1.35}
  .review{background:var(--panel2);border:1px solid var(--line);border-left:3px solid var(--violet);
    padding:9px 11px;border-radius:8px;font-size:.8125rem;margin-top:8px}
  .review b{color:var(--violet);display:inline-flex;align-items:center;gap:4px}
  .hide{display:none}.muted{color:var(--muted)}
  .flag-late{color:var(--amber);font-size:.75rem;font-weight:600}
  .demo{background:var(--warn-bg);border:1px solid var(--warn-bd);color:var(--warn-fg);border-radius:10px;
    padding:var(--sp-2) var(--sp-3);font-size:.8125rem;margin:var(--sp-3) 0;
    display:flex;gap:var(--sp-2);align-items:flex-start}
  .banner{background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    border:1px solid var(--line);border-radius:12px;padding:10px 14px;
    font-size:.8125rem;color:var(--muted);margin-top:14px;display:flex;gap:var(--sp-2);align-items:center}

  /* ============ CALENDAR ============ */
  /* ---- team schedule: who to show ----
     A row of one chip per person did not scale and wrapped onto two lines on a
     laptop. One trigger that states the current selection, opening the same
     popover shell every other menu in this app uses. */
  .tf-btn{display:inline-flex;align-items:center;gap:7px;flex:none;
    background:var(--panel2);background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3);
    border:1px solid var(--line-strong);border-radius:10px;color:var(--text);
    padding:0 11px;min-height:var(--tap);font:inherit;font-size:.8125rem;font-weight:600;
    cursor:pointer;white-space:nowrap;box-shadow:var(--glass-drop);
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease,background-color var(--dur-fast) ease}
  .tf-btn:hover{border-color:var(--brand)}
  .tf-btn:focus-visible{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  .tf-btn[aria-expanded="true"]{border-color:var(--brand);
    background:color-mix(in srgb,var(--brand) 12%,transparent);color:var(--brand2)}
  .tf-btn>.ic{color:var(--brand);flex:none}
  .tf-btn .tf-lbl{min-width:0;overflow:hidden;text-overflow:ellipsis;max-width:16ch}
  .tf-btn .caret{width:13px;height:13px;color:var(--muted);transition:transform var(--dur-fast) var(--ease)}
  .tf-btn[aria-expanded="true"] .caret{transform:rotate(180deg)}
  /* the faces of the first few selected people, so the button says who without words */
  .tf-faces{display:inline-flex;align-items:center}
  .tf-faces .avatar.sm{width:24px;height:24px;font-size:.625rem;
    box-shadow:0 0 0 2px var(--panel2);margin-left:-6px}
  .tf-faces .avatar.sm:first-child{margin-left:0}
  .tf-faces .avatar.sm.pic{font-size:14px}
  #teamPop{padding:6px;min-width:262px;max-width:320px}
  #teamPop .tp-head{display:flex;align-items:center;gap:7px;padding:6px 8px 8px;
    font-size:.6875rem;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(--muted)}
  #teamPop .tp-head .ic{color:var(--brand);width:14px;height:14px}
  #teamPop .tp-search{width:100%;background:var(--panel2);border:1px solid var(--line);
    border-radius:9px;padding:7px 10px;font:inherit;font-size:.8125rem;color:var(--text);
    outline:none;margin-bottom:5px}
  #teamPop .tp-search:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--ring)}
  #teamPop .tp-row{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:9px;
    cursor:pointer;font-size:.875rem;min-height:var(--tap);
    transition:background-color var(--dur-fast) ease}
  #teamPop .tp-row:hover,#teamPop .tp-row.hi{background:color-mix(in srgb,var(--brand) 13%,transparent)}
  #teamPop .tp-row:focus-within{background:color-mix(in srgb,var(--brand) 13%,transparent);
    box-shadow:inset 0 0 0 1px var(--brand)}
  #teamPop .tp-row .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #teamPop .tp-row .you{color:var(--brand2);font-weight:600}
  #teamPop .tp-row .n{margin-left:auto;font-size:.6875rem;color:var(--muted);flex:none}
  #teamPop .tp-all{font-weight:700}
  #teamPop .tp-sep{height:1px;background:var(--line);margin:5px 2px}
  #teamPop .tp-empty{padding:10px;color:var(--muted);font-size:.8125rem;text-align:center}
  /* ---- team schedule: day / week / year ---- */
  .teamhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px}
  .teamhead .sub{margin:0}
  /* week: one block per day, so a light day and a packed day are equally obvious */
  .wk-day{border:1px solid var(--line);border-radius:11px;margin-bottom:var(--sp-2);overflow:hidden;
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent)}
  .wk-day.today{border-color:var(--brand);box-shadow:0 0 0 1px color-mix(in srgb,var(--brand) 40%,transparent)}
  .wk-day.holiday{border-color:var(--hol-bd);background:var(--hol-bg)}
  .wk-day.sel .wk-hd{background:color-mix(in srgb,var(--brand) 13%,transparent)}
  .wk-hd{display:flex;align-items:center;gap:9px;padding:8px 12px;cursor:pointer;flex-wrap:wrap;
    border-bottom:1px solid transparent;font-size:.8125rem;
    transition:background-color var(--dur-fast) ease}
  .wk-hd:hover{background:color-mix(in srgb,var(--brand) 9%,transparent)}
  .wk-hd .dow{font-weight:700;color:var(--brand2)}
  .wk-hd .dnum{font-weight:500}
  .wk-hd .hol{color:var(--hol-fg);font-weight:600;font-size:.75rem}
  /* the count used to be plain muted text — easy to miss when scanning a
     whole week at a glance, so it's now a coloured pill like the rest of the
     app's badges; a free day gets the quiet/neutral variant, not the accent */
  .wk-hd .cnt{margin-left:auto;display:inline-flex;align-items:center;white-space:nowrap;
    padding:2px 10px;border-radius:12px;font-size:.75rem;font-weight:700;
    background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand2);
    border:1px solid color-mix(in srgb,var(--brand) 30%,transparent)}
  .wk-hd .cnt b{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,var(--brand2) 50%,transparent)}
  .wk-hd .cnt.empty{background:transparent;color:var(--muted);border-color:var(--line-strong);font-weight:600}
  .wk-toggle{flex:none;width:26px;height:26px;padding:0;display:grid;place-items:center;
    border:none;border-radius:7px;background:transparent;color:var(--muted);cursor:pointer;
    transition:background-color .15s ease,color .15s ease}
  .wk-toggle:hover{background:color-mix(in srgb,var(--brand) 12%,transparent);color:var(--text)}
  .wk-toggle .ic{transition:transform .18s var(--ease)}
  .wk-day.collapsed .wk-toggle .ic{transform:rotate(-90deg)}
  .wk-day.collapsed .wk-body{display:none}
  .wk-body{padding:9px 10px 2px;border-top:1px solid var(--line)}
  .wk-body .task{margin-bottom:7px}
  .wk-none{padding:7px 12px 9px;color:var(--muted);font-size:.75rem}
  /* ---- AI work review ----
     A report is a page of prose per person, so the sheet is a column and only
     the report itself scrolls: the title stays readable and Print / Close stay
     reachable, instead of sitting a full screen below the last paragraph where
     they have to be scrolled to. #kpiModal is laid out the same way. */
  #aiModal .sheet{display:flex;flex-direction:column;max-height:90vh}
  #aiModal .sheet>:not(#aiBody){flex:none}
  #aiBody{flex:1 1 auto;min-height:0;overflow:auto;
    margin:0 -4px;padding:0 4px}          /* room for a focus ring at the edges */
  /* now that they are pinned, both ends need a rule to sit against */
  #aiModal .modal-title{margin-bottom:var(--sp-3);padding-bottom:var(--sp-3);
    border-bottom:1px solid var(--line)}
  #aiModal .row.foot{margin-top:var(--sp-3);padding-top:var(--sp-3);
    border-top:1px solid var(--line)}
  /* Name, period and timestamp are one masthead rather than three siblings
     whose gaps had to be kept in step by hand: one margin below the group. */
  .ai-hd{padding-bottom:var(--sp-3);margin-bottom:var(--sp-4);
    border-bottom:1px solid var(--line)}
  /* space-between, not an auto margin on the badges. With both on one line it
     reads name-left / badges-right exactly as before — but when the badges
     wrap onto their own line they start at the left edge under the name,
     instead of being shoved to the far right by a margin with nothing left to
     push against. The wrap decides, so no width query has to guess at it. */
  .ai-head{display:flex;align-items:flex-start;justify-content:space-between;
    gap:var(--sp-2) var(--sp-3);flex-wrap:wrap}
  .ai-head .who{display:flex;align-items:center;gap:10px;min-width:0}
  .ai-head h3{font-size:1.0625rem;font-weight:700;line-height:1.35;overflow-wrap:anywhere}
  .ai-head .meta{font-size:.75rem;color:var(--muted);margin-top:3px}
  .ai-badges{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
  /* avatar circle gives the header something to anchor on besides text,
     and separates "who" (name+role) at a glance from "when" below it */
  .ai-avatar{flex:none;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;
    background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand2);
    border:1px solid color-mix(in srgb,var(--brand) 30%,transparent)}
  .ai-avatar .ic{width:18px;height:18px}
  .who-info{min-width:0}
  .who-sub{font-size:.75rem;color:var(--muted);margin-top:1px}
  /* the analysed period used to be buried mid-sentence in the meta line;
     it's the one date a manager actually needs to register, so it gets its
     own pill — the "generated at" timestamp is secondary and stays small */
  .ai-period{display:inline-flex;align-items:center;gap:6px;margin-top:var(--sp-2);padding:5px 12px;
    border-radius:20px;font-size:.8125rem;font-weight:600;color:var(--text);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);border:1px solid var(--line)}
  .ai-period .ic{width:14px;height:14px;color:var(--brand2);flex:none}
  .ai-period-n{color:var(--muted);font-weight:500}
  .ai-period-n::before{content:'·';margin-right:6px;opacity:.6}
  .ai-generated{display:flex;align-items:center;gap:5px;font-size:.6875rem;color:var(--muted);margin-top:var(--sp-1)}
  .ai-generated .ic{width:11px;height:11px}
  /* the headline is the one line a busy manager reads even if nothing else —
     an accent bar + tint make it read as a callout, not just bigger text */
  /* no top margin: .ai-hd above already owns the gap under the masthead */
  .ai-lede{font-size:1rem;font-weight:600;color:var(--text);line-height:1.5;margin:0 0 var(--sp-4);
    padding:var(--sp-3) var(--sp-4);border-left:3px solid var(--brand);border-radius:0 9px 9px 0;
    background:color-mix(in srgb,var(--brand) 8%,transparent)}
  .ai-sec{margin-bottom:var(--sp-4)}
  .ai-sec>h4{font-size:.6875rem;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
    color:var(--muted);margin-bottom:var(--sp-2);display:flex;align-items:center;gap:6px}
  .ai-sec>h4 .ic{width:13px;height:13px;color:var(--brand)}
  .ai-sec p{font-size:.875rem;line-height:1.65}
  .ai-sec ul{margin:0;padding-left:18px;font-size:.875rem;line-height:1.6}
  .ai-sec li{margin-bottom:var(--sp-1)}
  .ai-sec.pos>h4 .ic{color:var(--green)}
  /* watchouts/suggestions stand alone (not nested in a .ai-card already),
     so each becomes its own tinted callout box — colour plus icon plus
     label, never colour alone, so it still reads without relying on hue */
  /* one rhythm for every top-level block: the callouts sat on 24 while the
     grid and the plain sections sat on 16, so the column breathed unevenly
     depending on which kind of block happened to follow which */
  .ai-sec.warn,.ai-sec.action{padding:var(--sp-3) var(--sp-4);border-radius:12px;
    margin:var(--sp-4) 0}
  .ai-sec.warn{background:color-mix(in srgb,var(--amber) 8%,transparent);
    border:1px solid color-mix(in srgb,var(--amber) 25%,transparent)}
  .ai-sec.warn>h4 .ic{color:var(--amber)}
  .ai-sec.action{background:color-mix(in srgb,var(--brand) 7%,transparent);
    border:1px solid color-mix(in srgb,var(--brand) 22%,transparent)}
  .ai-sec.action>h4 .ic{color:var(--brand)}
  .ai-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:var(--sp-4);margin-bottom:var(--sp-4)}
  .ai-card{border:1px solid var(--line);border-radius:12px;padding:var(--sp-4);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent)}
  /* two aiPara/aiSec blocks stacked inside one card read as one grouped unit
     (e.g. focus+work_style, balance+strengths) rather than two more top-level
     sections, so they sit closer together and the last one carries no trailing
     gap before the card's own padding takes over */
  .ai-card>.ai-sec{margin-bottom:var(--sp-2)}
  .ai-card>.ai-sec:last-child{margin-bottom:0}
  /* the DISC block reads as a distinct, more numeric section vs. the prose
     above it, so it gets a divider instead of just another stacked margin */
  .ai-sec.disc-sec{border-top:1px solid var(--line);padding-top:var(--sp-4);margin-top:var(--sp-4)}
  /* DISC bars: four fixed rows, so the shape of the profile reads at a glance */
  /* The label column may shrink (it already ellipsises) and the value column
     hugs whatever it holds — a bare percentage here, a percentage plus its
     since-last-time delta in the comparison view. Both used to be fixed, so a
     narrow sheet squeezed the bar while 60px sat empty beside a two-digit
     number. */
  .disc-row{display:grid;grid-template-columns:minmax(0,136px) minmax(60px,1fr) auto;
    align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2);font-size:.75rem}
  .disc-row .k{font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .disc-row .bar{height:7px;border-radius:4px;background:color-mix(in srgb,var(--line) 55%,transparent);overflow:hidden}
  .disc-row .bar i{display:block;height:100%;border-radius:4px;background:var(--brand2);
    transition:width .5s var(--ease)}
  .disc-row.top .bar i{background:var(--brand)}
  .disc-row .v{text-align:right;color:var(--muted);font-variant-numeric:tabular-nums;min-width:34px}
  .disc-badge{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:14px;
    font-size:.75rem;font-weight:700;background:color-mix(in srgb,var(--brand) 14%,transparent);
    color:var(--brand2);border:1px solid color-mix(in srgb,var(--brand) 30%,transparent)}
  /* header-only quick-glance version, beside the confidence pill */
  .disc-badge.sm{padding:2px 8px;font-size:.6875rem;gap:4px}
  .disc-badge.sm .ic{width:12px;height:12px}
  .ai-conf{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:12px;
    font-size:.6875rem;font-weight:700;border:1px solid var(--line-strong);color:var(--muted)}
  .ai-conf.high{color:var(--green);border-color:color-mix(in srgb,var(--green) 45%,transparent)}
  .ai-conf.medium{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 45%,transparent)}
  .ai-conf.low{color:var(--red);border-color:color-mix(in srgb,var(--red) 45%,transparent)}
  .ai-note{display:flex;gap:7px;align-items:flex-start;font-size:.6875rem;line-height:1.55;
    color:var(--muted);background:var(--warn-bg);border:1px solid var(--warn-bd);
    border-radius:9px;padding:var(--sp-2) var(--sp-3);margin-top:var(--sp-3)}
  .ai-note .ic{flex:none;color:var(--warn-fg);width:14px;height:14px;margin-top:1px}
  /* evidence lines as small quote cards instead of a plain blockquote rule —
     easier to scan as a set of distinct data points backing the read above */
  .ai-quote{display:flex;align-items:flex-start;gap:8px;font-size:.8125rem;color:var(--text);
    background:color-mix(in srgb,var(--panel2) var(--glass-a),transparent);
    border:1px solid var(--line);border-radius:9px;padding:9px 11px;margin-bottom:6px;line-height:1.55}
  .ai-quote .ic{flex:none;width:13px;height:13px;color:var(--muted);margin-top:2px}
  .ai-sec .ai-quote:last-child{margin-bottom:0}
  /* (.ai-person + .ai-person used to stack the whole team into one scroll.
     aiTeamShowPage has paged one person at a time since, so no two of them are
     ever siblings and the separator rule had nothing left to match.) */
  .ai-fail{color:var(--red);font-size:.8125rem}
  /* analyze-again / view-history picker (aiReviewOne). Each row pairs a
     checkbox (pick two to compare) with a button (open just this one) —
     two separate controls, since a checkbox can't nest inside a button. */
  .ai-hist-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}
  .ai-hist-row{display:flex;align-items:center;gap:8px}
  .ai-hist-check{display:flex;flex:none;padding:4px}
  .ai-hist-check input{width:17px;height:17px;accent-color:var(--brand)}
  .ai-hist-open{flex:1;min-width:0;display:flex;justify-content:space-between;align-items:center;gap:var(--sp-3);
    padding:var(--sp-3);border-radius:10px;border:1px solid var(--line);background:var(--panel2);
    color:var(--text);font-family:inherit;font-size:.8125rem;cursor:pointer;text-align:left;
    transition:border-color .15s ease}
  .ai-hist-open:hover{border-color:var(--brand)}
  .ai-hist-open .muted{font-size:.6875rem;white-space:nowrap}
  /* PICKED. aiXSlotHtml has always put .on on the chosen row and nothing
     anywhere drew it, so clicking a report in a compare slot changed nothing
     at all on screen — the only way to know what you had picked was to
     remember it, and the Compare button quietly enabling itself was the sole
     hint that the click had registered. Border, tint AND a tick: the tick is
     what makes it readable without relying on the colour. */
  .ai-hist-open.on{border-color:var(--brand);
    background:color-mix(in srgb,var(--brand) 12%,var(--panel2));
    box-shadow:inset 0 0 0 1px var(--brand)}
  /* visibility, not display: the tick reserves its space either way, so rows
     do not jump sideways as the selection moves down the list */
  .ai-hist-open .pick{flex:none;width:15px;height:15px;color:var(--brand);visibility:hidden}
  .ai-hist-open.on .pick{visibility:visible}
  /* Two lines, for the narrow compare slots. The window is what identifies the
     dataset and the capture time is a footnote, but side by side in a column
     half the sheet wide they were competing for the same row and both ended up
     cramped against the middle. */
  .ai-hist-open.stack{justify-content:flex-start;align-items:flex-start;gap:var(--sp-2)}
  .ai-hist-open.stack .tx{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
  .ai-hist-open.stack .tx b{font-weight:600;font-variant-numeric:tabular-nums}
  .ai-hist-open.stack .tx small{font-size:.6875rem;color:var(--muted);font-variant-numeric:tabular-nums}
  /* A slot with a report in it reads as filled from across the card, so "A is
     set, B is not" is answerable at a glance instead of by reading two lists. */
  .ai-card.picked{border-color:var(--brand);
    background:color-mix(in srgb,var(--brand) 6%,var(--panel2))}
  /* The slot's own header: which slot, and once something is chosen, WHO and
     WHICH WINDOW — so both picks stay readable side by side without reopening
     either list. */
  .ai-slot-h{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;margin-bottom:var(--sp-2)}
  .ai-slot-h .n{flex:none;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;
    font-size:.6875rem;font-weight:800;color:var(--muted);
    border:1px solid var(--line-strong);background:var(--panel2)}
  .ai-slot-h .n.on{color:var(--on-brand);background:var(--brand);border-color:var(--brand)}
  .ai-slot-h .lbl{min-width:0;flex:1;font-size:.8125rem;font-weight:700;color:var(--text);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .ai-slot-h .lbl.empty{font-weight:600;color:var(--muted)}
  .ai-slot-h .rng{display:inline-flex;align-items:center;gap:4px;flex:none;
    font-size:.6875rem;font-weight:600;color:var(--brand2);white-space:nowrap;
    font-variant-numeric:tabular-nums}
  .ai-slot-h .rng .ic{width:12px;height:12px;flex:none}
  .ai-compare-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
    margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px solid var(--line)}
  .ai-period-choice{display:flex;gap:var(--sp-3);flex-wrap:wrap}
  .ai-period-choice .btn{flex:1;justify-content:center;min-width:150px}
  /* whole-team review: one page per person, prev/next + dots like What's New */
  /* .pgr* is the same control with a neutral name — the KPI drill-down reuses
     it, so the two pagers cannot drift apart visually. */
  .ai-team-pager,.pgr{display:flex;align-items:center;justify-content:space-between;gap:10px;
    margin-top:var(--sp-4);padding-top:14px;border-top:1px solid var(--line)}
  .ai-team-dots,.pgr-dots{display:flex;gap:6px;flex-wrap:wrap;justify-content:center}
  .ai-team-dot,.pgr-dot{width:8px;height:8px;padding:0;border:none;border-radius:50%;
    background:var(--line-strong);cursor:pointer;transition:width .2s ease,border-radius .2s ease}
  .ai-team-dot.on,.pgr-dot.on{background:var(--brand);width:20px;border-radius:4px}
  .ai-team-count,.pgr-count{font-size:.8125rem;color:var(--muted);font-variant-numeric:tabular-nums}
  .disc-row .delta{font-size:.6875rem;color:var(--muted);margin-left:6px;font-variant-numeric:tabular-nums}
  /* cross-person compare: one staff picker per slot, inside the same .ai-card
     used for the slot itself so it reads as one unit while empty */
  .ai-xselect{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line-strong);
    border-radius:9px;padding:8px 10px;min-height:var(--tap);font-size:.8125rem;font-family:inherit}
  @media print{
    body *{visibility:hidden}
    #aiModal,#aiModal *{visibility:visible}
    #aiModal{position:absolute;inset:0;background:#fff;color:#000}
    #aiModal .sheet{max-width:none;box-shadow:none;border:0;max-height:none;overflow:visible;display:block}
    /* the on-screen layout scrolls the report inside a fixed-height box; on
       paper it has to flow, or everything past the first page is cut off */
    #aiBody{overflow:visible;max-height:none;margin:0;padding:0}
    #aiModal .modal-title{border-bottom:1px solid #ccc}
    #aiModal .modal-title .iconbtn,#aiModal .row.foot{display:none}
  }
  .cal-head{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-3);flex-wrap:wrap}
  .cal-head .mname{font-size:1rem;font-weight:700;min-width:190px;text-align:center}
  .cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
  .cal-dow{color:var(--muted);font-size:.75rem;text-transform:uppercase;text-align:center;padding:4px 0}
  .cal-cell{background:var(--panel);background:color-mix(in srgb,var(--panel) var(--glass-a),transparent);
    -webkit-backdrop-filter:blur(10px) saturate(1.4);backdrop-filter:blur(10px) saturate(1.4);
    border:1px solid var(--line);border-radius:10px;height:132px;overflow:hidden;
    padding:7px;cursor:pointer;display:flex;flex-direction:column;gap:4px;box-shadow:var(--glass-in),var(--glass-drop);
    /* background/color follow the page's theme cross-fade (var(--dur), like .task) */
    transition:border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease,
    background-color var(--dur) ease,color var(--dur) ease}
  .cal-cell:hover{border-color:var(--brand);box-shadow:var(--shadow-sm),var(--glass-in),var(--glass-drop)}
  .cal-cell.pad{background:transparent;border:none;cursor:default;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
  .cal-cell.pad:hover{box-shadow:none}
  .cal-cell.today{border-color:var(--brand);box-shadow:0 0 0 1px var(--brand) inset}
  .cal-cell.sel{outline:2px solid var(--brand2);outline-offset:1px}
  .cal-date{font-size:.8125rem;font-weight:600;color:var(--muted);display:flex;justify-content:space-between;align-items:center}
  .cal-date .cnt{background:var(--panel2);border-radius:10px;padding:0 6px;font-size:.75rem;color:var(--brand2)}
  .cal-cell.today .cal-date{color:var(--text)}
  .chip{font-size:.75rem;padding:2px 6px;border-radius:5px;white-space:nowrap;overflow:hidden;
    text-overflow:ellipsis;line-height:1.45;display:flex;align-items:center;gap:3px}
  .chip .ic{width:12px;height:12px}
  .chip.s-Planned{background:var(--s-plan-bg);color:var(--s-plan-fg)}
  .chip.s-InProgress{background:var(--s-prog-bg);color:var(--s-prog-fg)}
  .chip.s-Done{background:var(--s-done-bg);color:var(--s-done-fg)}
  .chip.s-CarriedOver{background:var(--s-carry-bg);color:var(--s-carry-fg)}
  .chip.s-Leave{background:var(--s-leave-bg);color:var(--s-leave-fg)}
  .chip.more{background:transparent;color:var(--muted)}
  .cal-cell.holiday{background:var(--hol-bg);border-color:var(--hol-bd)}
  .chip.holiday{background:var(--hol-chip);color:var(--hol-fg);font-weight:600;white-space:normal;text-overflow:clip;
    line-height:1.25;flex:0 0 auto;align-items:flex-start}
  .chip.holiday>span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .holiday-banner{background:var(--hol-bg);border:1px solid var(--hol-bd);color:var(--hol-fg);border-radius:10px;
    padding:9px 13px;font-size:.875rem;font-weight:600;margin-bottom:var(--sp-3);
    display:flex;gap:var(--sp-2);align-items:center}

  /* ============ LOADING / SKELETON ============ */
  .loading{text-align:center;color:var(--muted);padding:40px}
  .spin{display:inline-block;width:15px;height:15px;border-radius:50%;vertical-align:-2px;
    border:2px solid var(--line);border-top-color:var(--brand);animation:spin .6s linear infinite}
  .spin.lg{width:26px;height:26px;border-width:3px}
  .sk{position:relative;overflow:hidden;background:var(--skel);background-size:300% 100%;
    animation:shimmer 1.3s linear infinite;border-radius:11px}
  .sk-task{height:70px;margin-bottom:var(--sp-2)}
  .sk-cell{height:132px;border-radius:10px}
  .sk-kpi{height:92px;border-radius:14px}
  .sk-line{height:14px;border-radius:6px;margin-bottom:8px}
  .sk-row{height:42px;border-radius:8px;margin-bottom:6px}
  /* ---- FIRST PAINT, BEFORE THE ROSTER ARRIVES ----
     A cold start (no cached bootstrap) used to show the logo, an empty gap
     where the role tabs belong, and three grey bars: every tab and toolbar
     button appeared only once getInitialData came back, so the chrome
     assembled itself under the user. paintBootChrome() draws the whole set on
     the first frame instead, and bootChromeLock() holds it inert - dimmed
     reads as "not ready yet", and the disabled attribute it sets (rather than
     pointer-events, which a keyboard walks straight past) is what keeps a
     press out until BOOT carries the categories and roster those handlers
     read. The build stamp is deliberately NOT part of this: only
     updateVersionBadge() ever unhides it, and it still refuses to until BOOT
     names Thanit. */
  #roleTabs.booting,.booting>.row,.booting .toolbar,.booting .sub{opacity:.55}
  /* progress ring: conic fill of --prog (registered so it transitions smoothly)
     with a masked hole — used for the first-load chip and the refresh state */
  @property --prog{syntax:'<percentage>';inherits:false;initial-value:0%}
  .ring{width:var(--rs,18px);height:var(--rs,18px);border-radius:50%;flex:none;display:inline-block;vertical-align:-2px;
    background:conic-gradient(var(--brand) var(--prog,0%),color-mix(in srgb,var(--line) 60%,transparent) 0);
    -webkit-mask:radial-gradient(farthest-side,transparent calc(100% - var(--rw,3px)),#000 calc(100% - var(--rw,3px)));
            mask:radial-gradient(farthest-side,transparent calc(100% - var(--rw,3px)),#000 calc(100% - var(--rw,3px)));
    transition:--prog 260ms var(--ease)}
  .ring.sm{--rs:14px;--rw:2.5px}
  /* while progress is still 0% the ring spins like the old spinner; once a read
     lands it becomes the fill-ring (the .spin background swap is not transitioned,
     and prefers-reduced-motion freezes it via the global rule). border:0 drops the
     border-spinner stroke .spin would otherwise draw on top of the conic arc — the
     ring must show one stroke, not two stacked ones. */
  .ring.spin{border:0;background:conic-gradient(var(--brand) 140deg,transparent 0);animation:spin .8s linear infinite}
  /* ---- LOAD CAPSULE ----
     Sits inline in the header beside the role tabs (งานของฉัน / ทีม) rather
     than floating over the content. #wnLoadProg is the What's New "speed"
     slide's copy of it, so the tour plays the real motion; the two duplicated
     every rule before, which is how they drifted apart, so they share one
     block now.

     One text node (.cap-tx) and one icon slot (.cap-ic), both written by
     capState(). The previous arrangement stacked four labels in the DOM and
     hid three with CSS - which is how the failure state came to reuse the
     refresh label and announce "กำลังอัปเดต…" in red while nothing was
     updating, and how a single aria-live region ended up holding every
     alternative wording at once.

     .hide must beat the base display, and .done/.err/.offline must beat
     .hide - equal specificity, so source order decides. */
  #loadProg,#wnLoadProg{display:flex;flex:none;align-items:center;gap:9px;padding:7px 14px;
    border-radius:22px;white-space:nowrap;
    background:color-mix(in srgb,var(--panel) var(--glass-a2),transparent);
    -webkit-backdrop-filter:blur(12px) saturate(1.4);backdrop-filter:blur(12px) saturate(1.4);
    border:1px solid var(--line);box-shadow:var(--shadow-sm),var(--glass-in);
    font-size:.8125rem;color:var(--muted);animation:capIn .25s var(--ease) both}
  #loadProg.on,#wnLoadProg.on{display:flex}
  #loadProg .cap-ic,#wnLoadProg .cap-ic{display:none;align-items:center}
  #loadProg .cap-ic .ic,#wnLoadProg .cap-ic .ic{width:18px;height:18px}
  /* every settled state speaks through its own icon, so the ring stands down */
  #loadProg.done,#loadProg.err,#loadProg.offline,
  #wnLoadProg.done,#wnLoadProg.err,#wnLoadProg.offline{display:flex}
  #loadProg.done .ring,#loadProg.err .ring,#loadProg.offline .ring,
  #wnLoadProg.done .ring,#wnLoadProg.err .ring,#wnLoadProg.offline .ring{display:none}
  #loadProg.done .cap-ic,#loadProg.err .cap-ic,#loadProg.offline .cap-ic,
  #wnLoadProg.done .cap-ic,#wnLoadProg.err .cap-ic,#wnLoadProg.offline .cap-ic{display:inline-flex}
  /* done: green tick, held ~2s so the completion is actually seen */
  #loadProg.done,#wnLoadProg.done{border-color:color-mix(in srgb,var(--green) 50%,transparent);
    box-shadow:0 0 0 1px color-mix(in srgb,var(--green) 35%,transparent),var(--shadow-sm),var(--glass-in)}
  #loadProg.done .cap-tx,#wnLoadProg.done .cap-tx{color:var(--green);font-weight:600}
  #loadProg.done .cap-ic .ic,#wnLoadProg.done .cap-ic .ic{color:var(--green);
    animation:popCheck .4s cubic-bezier(.34,1.56,.64,1) both}
  @keyframes popCheck{0%{transform:scale(.2);opacity:0}60%{transform:scale(1.18);opacity:1}100%{transform:scale(1)}}
  /* failed: red, one shake, and it now says so in words instead of keeping the
     "updating" label - held long enough to actually read */
  #loadProg.err,#wnLoadProg.err{border-color:var(--red);color:var(--red);
    box-shadow:0 0 0 1px color-mix(in srgb,var(--red) 45%,transparent),
               0 2px 16px color-mix(in srgb,var(--red) 25%,transparent),var(--glass-in);
    animation:capShake .45s ease}
  #loadProg.err .cap-tx,#wnLoadProg.err .cap-tx{color:var(--red);font-weight:600}
  #loadProg.err .cap-ic .ic,#wnLoadProg.err .cap-ic .ic{color:var(--red)}
  /* offline: amber and still, not red and jolting. Losing the network is a
     condition to report, not a fault to react to - and the banner is already
     shouting about it, so the capsule only needs to explain why the data on
     screen is not moving. */
  #loadProg.offline,#wnLoadProg.offline{border-color:color-mix(in srgb,var(--amber) 60%,transparent);
    color:var(--amber);
    box-shadow:0 0 0 1px color-mix(in srgb,var(--amber) 35%,transparent),var(--shadow-sm),var(--glass-in)}
  #loadProg.offline .cap-tx,#wnLoadProg.offline .cap-tx{color:var(--amber);font-weight:600}
  #loadProg.offline .cap-ic .ic,#wnLoadProg.offline .cap-ic .ic{color:var(--amber)}
  /* Declared LAST on purpose. capState() and capHide() keep .hide mutually
     exclusive with on/done/err/offline, so this never competes in practice -
     but when it did, .on sat after it and won, which is how a finished load
     left the capsule parked in the header claiming "กำลังอัปเดต…" for the rest
     of the session. If the invariant is ever broken again the capsule
     disappears, which is the safe way to be wrong. */
  /* Done -> gone as a fade rather than a cut. It has to REPLACE the animation
     rather than just set opacity: the base rule runs capIn with fill-mode
     "both", which keeps its final opacity:1 applied for good, and a filled
     animation outranks a plain declaration - so .cap-fade{opacity:0} would
     simply lose. Duration is mirrored by CAP_FADE_MS in the script; the global
     prefers-reduced-motion rule collapses it to a cut, which is the right
     answer for someone who asked for less movement. */
  #loadProg.cap-fade,#wnLoadProg.cap-fade{animation:capFadeOut 2s var(--ease) forwards}
  @keyframes capFadeOut{from{opacity:1}to{opacity:0}}
  #loadProg.hide,#wnLoadProg.hide{display:none}
  /* the capsule shakes in place beside the tabs — offsets from its own spot */
  @keyframes capShake{0%,100%{transform:translateX(0)}
    20%{transform:translateX(-9px)}40%{transform:translateX(9px)}
    60%{transform:translateX(-6px)}80%{transform:translateX(6px)}}
  /* entrance pop: the capsule fades/scales in where it sits next to the tabs */
  @keyframes capIn{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:none}}

  /* ============ TOASTS ============ */
  .toasts{position:fixed;right:16px;bottom:16px;z-index:80;display:flex;flex-direction:column;gap:var(--sp-2);
    max-width:min(380px,calc(100vw - 32px));pointer-events:none;
    padding-bottom:env(safe-area-inset-bottom)}
  /* iOS-26-style liquid glass toast: a strongly-rounded floating capsule with a
     bright top edge-light and a soft diagonal sheen (::before), a glossy circular
     icon lens, and a glowing inset countdown pill. Heavier backdrop blur makes the
     sheet behind genuinely refract through it. */
  .toast{background:color-mix(in srgb,var(--panel) calc(var(--glass-a3) + 7%),transparent);
    -webkit-backdrop-filter:blur(18px) saturate(1.8);backdrop-filter:blur(18px) saturate(1.8);
    border:1px solid color-mix(in srgb,var(--line) 65%,transparent);
    border-radius:20px;padding:12px 14px;font-size:.875rem;color:var(--text);
    box-shadow:0 14px 36px rgba(0,0,0,.30),
      inset 0 1px 0 color-mix(in srgb,#fff 22%,transparent),
      inset 0 -1px 0 color-mix(in srgb,#000 16%,transparent);
    position:relative;overflow:hidden;
    display:flex;gap:11px;align-items:flex-start;
    pointer-events:auto;animation:toastIn .5s cubic-bezier(.3,1.25,.42,1) both;
    transition:box-shadow .25s ease,background-color var(--dur) ease,color var(--dur) ease}
  /* specular top edge-light + diagonal sheen, clipped by the rounded corners */
  .toast::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
    background:radial-gradient(130% 90% at 12% -10%,color-mix(in srgb,#fff 26%,transparent),transparent 55%),
      linear-gradient(118deg,color-mix(in srgb,#fff 8%,transparent),transparent 38%,transparent 72%,
        color-mix(in srgb,#000 5%,transparent) 108%)}
  .toast:hover{box-shadow:0 18px 42px rgba(0,0,0,.36),
      inset 0 1px 0 color-mix(in srgb,#fff 26%,transparent),
      inset 0 -1px 0 color-mix(in srgb,#000 16%,transparent)}
  .toast>*{z-index:1}
  .toast.out{animation:toastOut .22s ease both}
  /* glossy lens for the status icon — accent tint + inner highlight + soft glow */
  .toast .ic-chip{flex:0 0 auto;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;
    background:radial-gradient(130% 130% at 30% 20%,color-mix(in srgb,#fff 32%,transparent),transparent 46%),
      color-mix(in srgb,var(--brand) 24%,transparent);
    color:var(--brand);
    box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 38%,transparent),
      0 3px 12px color-mix(in srgb,var(--brand) 26%,transparent)}
  .toast.ok .ic-chip{background:radial-gradient(130% 130% at 30% 20%,color-mix(in srgb,#fff 32%,transparent),transparent 46%),color-mix(in srgb,var(--green) 24%,transparent);
    color:var(--green);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 38%,transparent),0 3px 12px color-mix(in srgb,var(--green) 26%,transparent)}
  .toast.err .ic-chip{background:radial-gradient(130% 130% at 30% 20%,color-mix(in srgb,#fff 32%,transparent),transparent 46%),color-mix(in srgb,var(--red) 24%,transparent);
    color:var(--red);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 38%,transparent),0 3px 12px color-mix(in srgb,var(--red) 26%,transparent)}
  .toast.warn .ic-chip{background:radial-gradient(130% 130% at 30% 20%,color-mix(in srgb,#fff 32%,transparent),transparent 46%),color-mix(in srgb,var(--amber) 24%,transparent);
    color:var(--amber);box-shadow:inset 0 1px 0 color-mix(in srgb,#fff 38%,transparent),0 3px 12px color-mix(in srgb,var(--amber) 26%,transparent)}
  .toast .tx{flex:1;min-width:0;word-break:break-word;padding-top:6px}
  /* Undo toast: wider to fit the button, uses amber tint to signal caution */
  .undo-toast{gap:8px;padding:10px 12px}
  .undo-toast .undo-btn{margin-top:4px;padding:0 12px;min-height:28px;font-size:.8125rem;
    border:1px solid var(--amber);color:var(--amber);border-radius:8px;
    background:color-mix(in srgb,var(--amber) 12%,transparent);
    cursor:pointer;font-weight:600;white-space:nowrap;
    transition:background var(--dur-fast) ease,transform 110ms ease}
  .undo-toast .undo-btn:hover{background:color-mix(in srgb,var(--amber) 24%,transparent);transform:scale(1.03)}
  .undo-toast .undo-btn:active{transform:scale(.97)}

  /* ============ ANIMATIONS ============ */
  @keyframes spin{to{transform:rotate(360deg)}}
  @keyframes shimmer{0%{background-position:150% 0}100%{background-position:-150% 0}}
  @keyframes fadeIn{from{opacity:0}to{opacity:1}}
  @keyframes fadeOut{from{opacity:1}to{opacity:0}}
  @keyframes popIn{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
  @keyframes popOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(8px) scale(.98)}}
  @keyframes toastIn{from{opacity:0;transform:translateX(28px) translateY(8px) scale(.93)}to{opacity:1;transform:none}}
  @keyframes toastOut{from{opacity:1;transform:none}to{opacity:0;transform:translateX(26px) scale(.97)}}
  @keyframes riseIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
  .anim{animation:riseIn var(--dur-slow) var(--ease) both}
  .stagger>*{animation:riseIn var(--dur-slow) var(--ease) both}
  .stagger>*:nth-child(1){animation-delay:.02s}.stagger>*:nth-child(2){animation-delay:.05s}
  .stagger>*:nth-child(3){animation-delay:.08s}.stagger>*:nth-child(4){animation-delay:.11s}
  .stagger>*:nth-child(5){animation-delay:.14s}.stagger>*:nth-child(6){animation-delay:.17s}
  .stagger>*:nth-child(7){animation-delay:.2s}.stagger>*:nth-child(8){animation-delay:.23s}
  .stagger>*:nth-child(n+9){animation-delay:.26s}

  /* Breakpoints checked at 375 / 768 / 1024 / 1440. Gutters widen with the viewport. */
  @media(min-width:1024px){ .wrap,.topin{padding-left:24px;padding-right:24px} }
  @media(max-width:1024px){ .cal-cell,.sk-cell{height:118px} }
  @media(max-width:768px){
    .kpis{grid-template-columns:repeat(2,1fr)}.g2{grid-template-columns:1fr}.g3{grid-template-columns:1fr}
    .cal-cell,.sk-cell{height:96px}
    .chip:not(.holiday){display:none}
    .chip.holiday{font-size:.6875rem}
    .task{flex-wrap:wrap}.task .right{margin-left:0;width:100%;display:flex;flex-wrap:wrap;gap:6px}
    .task .right .btn.sm:not(.icon){padding:0 8px;font-size:.75rem}
    .task .right .statsel{font-size:.75rem}
    .toasts{left:16px;right:16px;max-width:none}
    .cal-head .mname{min-width:0;flex:1}
  }
  /* Glass on mobile/coarse pointers: keep the translucent tint (the drifting orbs
     still show through it) but drop the per-element backdrop blur on small,
     numerous surfaces — dozens of tiny blur samplers per frame are the classic
     mobile scroll-jank source. The few big surfaces (header, sheet, pop, cards,
     KPIs, toasts) keep their blur. */
  @media (pointer:coarse), (max-width:768px){
    .task,.cal-cell,.statsel,.iconbtn,.toolbar input,.toolbar select,
    .field input,.field select,.field textarea,.btn.ghost,.banner,.roletabs{
      -webkit-backdrop-filter:none;backdrop-filter:none}
  }

  @media(max-width:520px){
    /* bottom-sheet picker: full-width panel sliding up from the bottom edge,
       with a drag handle and a denser 4-column grid */
    .pop{position:fixed;left:0;right:0;top:auto;bottom:0;width:100%;min-width:0;max-width:none;
      max-height:min(78vh,560px);border-radius:20px 20px 0 0;
      border-left:none;border-right:none;border-bottom:none;
      padding:10px 14px calc(14px + env(safe-area-inset-bottom));
      box-shadow:0 -12px 40px rgba(0,0,0,.45);animation:popUp .22s var(--ease) both}
    .pop::before{content:"";display:block;width:36px;height:4px;border-radius:2px;
      background:var(--line-strong);margin:0 auto 10px}
    .pop-head{padding-bottom:8px;margin-bottom:8px}
    .swatches{grid-template-columns:repeat(4,1fr);gap:8px}
    .sw .dot{height:34px;border-radius:10px}
    .sw{padding:4px 2px 6px;gap:6px}
    /* the date picker joins the bottom-sheet family — full width like the palette */
    .pop.pick-pop{width:100%;min-width:0;max-width:none;padding-bottom:calc(16px + env(safe-area-inset-bottom))}
    .pick-dow span{font-size:.75rem}
    .pick-day{font-size:.9375rem}
  }
  @keyframes popUp{from{transform:translateY(18px);opacity:.35}to{transform:none;opacity:1}}
  @media(max-width:400px){
    .exp-u{display:none}   /* the number still shows; only its unit label goes */
    .wrap,.topin{padding-left:12px;padding-right:12px}
    .cal-grid{gap:4px}.cal-cell,.sk-cell{height:78px}.cal-cell{padding:5px}
    .roletabs button{padding:0 10px;font-size:.75rem}
    .task .right{gap:4px}
    .task .right .btn.sm{min-height:30px}
    .task .right .btn.sm:not(.icon){padding:0 5px;font-size:.6875rem}
    /* var(--tap) not a fixed px: it already resolves to 44px under
       @media(pointer:coarse), so a touch phone at this width still gets a
       proper touch target instead of being squeezed to fit a narrow window. */
    .task .right .btn.sm.icon{width:var(--tap);min-width:var(--tap)}
    .task .title{font-size:.8125rem}
    .task .meta{font-size:.75rem}
  }

  /* Medium-to-small widths: the signed-in name beside the avatar is the widest
     header item and alone forces the whole .who row to wrap below the tabs on
     sub-~740px windows. The avatar keeps opening the profile, so drop the
     duplicate name text where the row no longer fits. */
  @media(max-width:700px){
    .topin>.who .who-edit{display:none}
    .wfhbtn .wfh-label{display:none}
    .wfhbtn{padding:0 9px}
  }
  /* Phones: keep the header to two rows and give the content its space back —
     tighter top-bar rhythm, and hide the advisory cutoff note (redundant
     context while planning; it reappears at wider widths). */
  @media(max-width:520px){
    .topin{padding-top:8px;padding-bottom:8px;gap:6px}
    .topin>.who{gap:6px}
    .roletabs button{padding:0 8px}
    /* the update capsule's label ("กำลังโหลด…" / "อัปเดตเสร็จสิ้น") was the
       widest thing in the bar and alone pushed refresh+capsule onto their own
       row, leaving three rows total instead of the two this section already
       promises above — the ring/check icon alone still says loading vs. done */
    #loadProg{padding:0;width:var(--tap);min-width:var(--tap);height:var(--tap);
      justify-content:center;border-radius:50%;gap:0}
    #loadProg .cap-tx{display:none}
    .toolbar{margin:10px 0;row-gap:6px}
    .cutoff{display:none}
    #staffView>div.row{margin:8px 0!important}   /* inline 10px margin on the view tabs */
    .task .right{gap:5px}
    .task .right .btn.sm{min-height:32px}
    .task .right .btn.sm:not(.icon){padding:0 6px;font-size:.6875rem}
    .task .right .btn.sm.icon{width:var(--tap);min-width:var(--tap)}
    .offline-banner{font-size:.75rem;padding:6px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .cal-head{gap:6px;flex-wrap:wrap}
    .tablewrap{position:relative}
    .tablewrap::after{content:'';position:absolute;top:0;right:0;bottom:0;width:24px;
      background:linear-gradient(90deg,transparent,var(--bg));pointer-events:none;border-radius:0 4px 4px 0}
  }

  /* ============ COMPACT LAPTOP FIT (13–15" screens) ============
     13–15" laptops are wide enough at 1280–1920px but lose ~120–200px of
     height to browser chrome, leaving only ~600–780px usable (1366×768,
     1280×800, 1440×900, and 125% Windows scaling of 1080p). These tiers
     tighten the vertical rhythm on desktop-width, short-height viewports:
     slimmer sticky header, tighter section/toolbar spacing, denser rows and
     table cells, a shorter month grid — without shrinking type or dropping
     below the 24px touch-target floor. Wider/taller screens are untouched. */
  @media (min-width:1024px) and (max-height:880px){
    .topin{padding-top:8px;padding-bottom:8px}
    .wrap{padding-bottom:28px}
    h2.section{margin:12px 0 2px}
    .sub{margin-bottom:8px}
    .toolbar{margin:8px 0}
    #staffView>div.row{margin:8px 0!important}   /* overrides the inline 10px margin on the view tabs */
    .cal-head{margin-bottom:10px}
    .cal-cell,.sk-cell{height:110px}
    .task{padding:9px 11px;gap:10px;margin-bottom:6px}
    .task .meta{margin-top:2px}
    .sk-task{height:56px;margin-bottom:6px}
    .sk-row{height:34px;margin-bottom:4px}
    .kpi{padding:12px 14px}
    th,td{padding:8px 10px}
    .card{padding:12px 14px}
    .banner{padding:7px 12px}
    .holiday-banner{padding:7px 12px;margin-bottom:8px}
    .demo{margin:8px 0;padding:6px 10px}
    .empty{padding:18px}
    .modal .sheet{padding:16px}
  }
  /* even shorter: maximised windows on 768/800px-tall panels squeeze further */
  @media (min-width:1024px) and (max-height:720px){
    .topin{padding-top:6px;padding-bottom:6px}
    .wrap{padding-bottom:18px}
    h2.section{margin:9px 0 1px}
    .sub{margin-bottom:6px}
    .toolbar{margin:6px 0}
    #staffView>div.row{margin:6px 0!important}
    .cal-head{margin-bottom:8px}
    .cal-cell,.sk-cell{height:92px}
    .task{padding:7px 9px;gap:8px;margin-bottom:5px}
    .task .meta{margin-top:1px}
    .task .title{font-size:.875rem}
    .task .statsel,.task .btn{min-height:28px}
    .sk-task{height:48px;margin-bottom:5px}
    .sk-kpi{height:74px}
    .sk-row{height:28px;margin-bottom:3px}
    .kpi{padding:10px 12px}
    .kpi .n{font-size:1.5rem}
    th,td{padding:7px 9px}
    .card{padding:10px 12px}
    .banner{padding:6px 10px}
    .holiday-banner{padding:6px 10px;margin-bottom:6px}
    .demo{margin:6px 0;padding:5px 9px}
    .empty{padding:14px}
    .modal .sheet{padding:14px}
    .modal-title{margin-bottom:10px}
  }
  @media (prefers-reduced-motion: reduce){
    *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;
      transition-duration:.01ms!important;scroll-behavior:auto!important}
    #ambient .blob{animation:none}
    /* ...except the one animation that IS the message rather than decoration.
       The "+N EXP" pop is a node that exists only while its animation runs —
       at .01ms it was created and removed inside a single frame, so anyone with
       reduced motion turned on (it is on by default on a phone set to reduce
       motion, and whenever Windows has "show animations" off) never saw a
       single +EXP in the app, ever. It keeps its full duration here and gives
       up the TRAVEL instead, over in its own rule near .exp-pop. */
    .exp-pop{animation-duration:1.5s!important}
  }
