/* ==========================================================================
   Physics Way — interface
   Apple Human Interface Guidelines, translated honestly to the web:
   system typography, grouped-inset lists with separators inset to the text,
   translucent materials over content, fills instead of borders, and restrained
   spring motion. Brand accent is Egyptian blue, overridable from Branding.
   ========================================================================== */

:root{
  /* --- brand (overridable at runtime from Admin → Branding) -------------- */
  --accent:#1B39B0;
  --accent-2:#2E4CC4;
  --accent-soft:color-mix(in srgb, var(--accent) 11%, #FFFFFF);
  --accent-line:color-mix(in srgb, var(--accent) 26%, #FFFFFF);
  --on-accent:#FFFFFF;

  --ok:#1B7F4B; --ok-soft:#E4F3EA;
  --warn:#8A5A00; --warn-soft:#FAF0DC;
  --crit:#B3261E; --crit-soft:#FBE9E7;

  /* --- Apple system greys ---------------------------------------------- */
  --bg:#F2F2F7;                 /* systemGroupedBackground */
  --surface:#FFFFFF;            /* secondarySystemGroupedBackground */
  --surface-2:#F2F2F7;
  --surface-3:#E9E9EF;
  --surface-4:#DCDCE3;
  --label:#000000;
  --label-2:rgba(60,60,67,.62); /* secondaryLabel */
  --label-3:rgba(60,60,67,.46); /* tertiaryLabel */
  --label-4:rgba(60,60,67,.32); /* quaternaryLabel */
  --line:rgba(60,60,67,.16);    /* separator */
  --line-2:rgba(60,60,67,.28);  /* opaqueSeparator */

  --glass:rgba(249,249,251,.72);
  --glass-line:rgba(60,60,67,.16);
  --sidebar:#F2F2F7;

  /* flat in flow; soft only when floating */
  --sh-1:none;
  --sh-2:0 2px 10px rgba(0,0,0,.07);
  --sh-3:0 12px 40px rgba(0,0,0,.16), 0 2px 8px rgba(0,0,0,.06);

  --r-s:10px; --r-m:14px; --r-l:18px; --r-xl:26px;
  --nav-w:260px; --head-h:54px;
  /* The phone chat drawer's height. Shared, because the board's controls
     have to sit exactly above it. */
  --chat-drawer-h:min(60dvh,440px);
  --ease:cubic-bezier(.32,.72,0,1);
  --spring:cubic-bezier(.34,1.42,.5,1);

  --font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI Variable Text","Segoe UI",system-ui,sans-serif;
  --font-d:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;
  --font-ar:"Cairo",-apple-system,"SF Arabic","Segoe UI",sans-serif;
  --font-m:ui-monospace,"SF Mono","Cascadia Mono","Segoe UI Mono",Menlo,monospace;
  color-scheme:light;
}

/* The dark palette is retired. Every rule that used to bring it in is gated on
   [data-theme="dark"], and nothing sets that any more — index.html ships
   data-theme="light" and applyTheme pins it there. The declarations are left in
   place rather than torn out so the file still shows what the second palette
   was, should it ever be wanted again. */
@media (prefers-color-scheme:dark){
  :root[data-theme="dark"]{
    --accent:#7E96F5; --accent-2:#93A7F8;
    --accent-soft:color-mix(in srgb, var(--accent) 22%, #000000);
    --accent-line:color-mix(in srgb, var(--accent) 40%, #000000);
    --on-accent:#0A0A0C;
    --ok:#30D158; --ok-soft:#122A18;
    --warn:#FFD426; --warn-soft:#2C2411;
    --crit:#FF6961; --crit-soft:#2E1614;
    --bg:#000000; --surface:#1C1C1E; --surface-2:#1C1C1E;
    --surface-3:#2C2C2E; --surface-4:#3A3A3C;
    --label:#FFFFFF;
    --label-2:rgba(235,235,245,.62);
    --label-3:rgba(235,235,245,.45);
    --label-4:rgba(235,235,245,.3);
    --line:rgba(84,84,88,.48);
    --line-2:rgba(84,84,88,.68);
    --glass:rgba(28,28,30,.74);
    --glass-line:rgba(84,84,88,.5);
    --sidebar:#0E0E10;
    --sh-2:0 2px 10px rgba(0,0,0,.5);
    --sh-3:0 16px 44px rgba(0,0,0,.7), 0 2px 8px rgba(0,0,0,.4);
    color-scheme:dark;
  }
}
:root[data-theme="dark"]{
  --accent:#7E96F5; --accent-2:#93A7F8;
  --accent-soft:color-mix(in srgb, var(--accent) 22%, #000000);
  --accent-line:color-mix(in srgb, var(--accent) 40%, #000000);
  --on-accent:#0A0A0C;
  --ok:#30D158; --ok-soft:#122A18;
  --warn:#FFD426; --warn-soft:#2C2411;
  --crit:#FF6961; --crit-soft:#2E1614;
  --bg:#000000; --surface:#1C1C1E; --surface-2:#1C1C1E;
  --surface-3:#2C2C2E; --surface-4:#3A3A3C;
  --label:#FFFFFF;
  --label-2:rgba(235,235,245,.62);
  --label-3:rgba(235,235,245,.45);
  --label-4:rgba(235,235,245,.3);
  --line:rgba(84,84,88,.48);
  --line-2:rgba(84,84,88,.68);
  --glass:rgba(28,28,30,.74);
  --glass-line:rgba(84,84,88,.5);
  --sidebar:#0E0E10;
  --sh-2:0 2px 10px rgba(0,0,0,.5);
  --sh-3:0 16px 44px rgba(0,0,0,.7), 0 2px 8px rgba(0,0,0,.4);
  color-scheme:dark;
}
/* ------------------------------------------------------------------ base */
*{box-sizing:border-box;}
html,body{height:100%;max-width:100%;}
img,svg,video,canvas{max-width:100%;}
/* Long codes, emails and URLs wrap instead of widening the page. */
.t,.s,.nm,.ti,.sn,.who .rl,.bub,.lede,.empty p{overflow-wrap:anywhere;}
body{
  margin:0;background:var(--bg);color:var(--label);font-family:var(--font);
  font-size:16px;line-height:1.47;letter-spacing:-.011em;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  overscroll-behavior-y:none;
}
html[dir="rtl"] body{font-family:var(--font-ar);letter-spacing:0;}
h1,h2,h3,h4{margin:0;font-family:var(--font-d);line-height:1.16;font-weight:600;letter-spacing:-.021em;}
h1{font-size:1.75rem;letter-spacing:-.026em;}
h2{font-size:1.32rem;}
h3{font-size:1.06rem;}
p{margin:0;}
a{color:var(--accent);text-decoration:none;}
button,input,textarea,select{font:inherit;color:inherit;letter-spacing:inherit;}
button{cursor:pointer;border:0;background:none;padding:0;}
:focus-visible{outline:3px solid var(--accent-line);outline-offset:2px;border-radius:8px;}
::selection{background:var(--accent-line);}
.mono{font-family:var(--font-m);font-variant-numeric:tabular-nums;}
.ltr{direction:ltr;unicode-bidi:isolate;}
[hidden]{display:none !important;}

*::-webkit-scrollbar{width:9px;height:9px;}
*::-webkit-scrollbar-track{background:transparent;}
*::-webkit-scrollbar-thumb{background:var(--label-4);border-radius:99px;
  border:2.5px solid transparent;background-clip:content-box;}
*::-webkit-scrollbar-thumb:hover{background:var(--label-3);background-clip:content-box;}

/* ------------------------------------------------------------- primitives */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  height:36px;padding:0 15px;border-radius:var(--r-s);
  font-size:.94rem;font-weight:500;letter-spacing:-.01em;
  background:var(--surface-3);color:var(--label);white-space:nowrap;
  transition:transform .2s var(--spring),background .18s var(--ease),opacity .18s;
}
.btn:hover{background:var(--surface-4);}
.btn:active{transform:scale(.96);}
.btn[disabled]{opacity:.4;pointer-events:none;}
.btn.pri{background:var(--accent);color:var(--on-accent);font-weight:600;}
.btn.pri:hover{background:var(--accent-2);}
.btn.ghost{background:transparent;color:var(--accent);}
.btn.ghost:hover{background:var(--accent-soft);}
.btn.danger{background:transparent;color:var(--crit);}
.btn.danger:hover{background:var(--crit-soft);}
.btn.sm{height:29px;padding:0 11px;font-size:.83rem;border-radius:8px;}
.btn.lg{height:48px;padding:0 24px;font-size:1.03rem;font-weight:600;border-radius:var(--r-m);}
.btn.block{width:100%;}

.field{display:flex;flex-direction:column;gap:7px;}
.field > label{font-size:.83rem;font-weight:500;color:var(--label-2);}
.input,.select,.textarea{
  width:100%;background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-s);
  padding:11px 14px;font-size:1rem;transition:border-color .18s,box-shadow .18s;
  -webkit-appearance:none;appearance:none;
}
.select{
  background-image:linear-gradient(45deg,transparent 50%,var(--label-3) 50%),
                   linear-gradient(135deg,var(--label-3) 50%,transparent 50%);
  background-position:calc(100% - 17px) 50%, calc(100% - 12px) 50%;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-inline-end:34px;
}
html[dir="rtl"] .select{background-position:17px 50%,12px 50%;
  padding-inline-end:14px;padding-inline-start:34px;}
.textarea{resize:vertical;min-height:100px;line-height:1.5;}
.input:focus,.select:focus,.textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3.5px var(--accent-soft);
}
.input::placeholder,.textarea::placeholder{color:var(--label-3);}
.hint{font-size:.83rem;color:var(--label-2);}
.hint.err{color:var(--crit);}
.hint.ok{color:var(--ok);}

/* ------------------------------------------------ a field that needs work */
/* The browser's own "Please fill out this field" is a grey system bubble that
   points at the wrong place on a phone, vanishes at the next tap, and is the
   one piece of text in the app that cannot be translated. It is cancelled in
   app.js and answered with this: the box itself goes red, and the reason sits
   under it in the same place every other hint does. */
.field.bad .input,.field.bad .select,.field.bad .textarea{
  border-color:var(--crit);animation:fieldNudge .3s var(--ease);
}
.field.bad .input:focus,.field.bad .select:focus,.field.bad .textarea:focus{
  border-color:var(--crit);box-shadow:0 0 0 3.5px var(--crit-soft);
}
/* One nudge rather than a wobble: enough to say which box moved, not enough
   to read as a telling-off. */
@keyframes fieldNudge{
  30%{transform:translateX(-3px);}65%{transform:translateX(3px);}100%{transform:translateX(0);}
}
.fmsg{
  display:flex;align-items:flex-start;gap:6px;margin:0;
  font-size:.83rem;font-weight:500;color:var(--crit);line-height:1.4;
  animation:fmsgIn .22s var(--ease) both;
}
.fmsg::before{
  content:"!";flex:none;width:15px;height:15px;margin-block-start:1px;
  border-radius:50%;background:var(--crit);color:var(--surface);
  font-size:.66rem;font-weight:800;line-height:1;display:grid;place-items:center;
}
@keyframes fmsgIn{from{opacity:0;transform:translateY(-3px);}}
@media (prefers-reduced-motion:reduce){
  .field.bad .input,.field.bad .select,.field.bad .textarea,.fmsg{animation:none;}
}

/* iOS segmented control */
.seg{display:inline-flex;background:var(--surface-3);border-radius:9px;padding:2px;gap:2px;}
.seg button{
  padding:6px 14px;border-radius:7px;font-size:.87rem;font-weight:500;color:var(--label);
  transition:background .2s var(--ease),box-shadow .2s;white-space:nowrap;
}
.seg button[aria-pressed="true"]{
  background:var(--surface);font-weight:600;
  box-shadow:0 3px 8px rgba(0,0,0,.12),0 3px 1px rgba(0,0,0,.04);
}
:root[data-theme="dark"] .seg button[aria-pressed="true"],
:root[data-theme="dark"] .seg button[aria-pressed="true"]{
  background:var(--surface-4);box-shadow:none;
}

.chip{
  display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:99px;
  background:var(--surface-3);color:var(--label);font-size:.85rem;font-weight:500;
  white-space:nowrap;transition:background .18s,color .18s;
}
button.chip:hover{background:var(--surface-4);}
.chip.on{background:var(--accent);color:var(--on-accent);font-weight:600;}
.chip .n{font-variant-numeric:tabular-nums;opacity:.75;}

.pill{
  display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:99px;
  font-size:.7rem;font-weight:600;white-space:nowrap;text-transform:capitalize;
}
.pill.open{background:var(--warn-soft);color:var(--warn);}
.pill.claimed,.pill.answered{background:var(--accent-soft);color:var(--accent);}
.pill.resolved{background:var(--ok-soft);color:var(--ok);}
.pill.reopened{background:var(--crit-soft);color:var(--crit);}
.pill.waiting_user{background:var(--surface-3);color:var(--label-2);}
.pill.late,.pill.high{background:var(--crit-soft);color:var(--crit);}
.pill.normal{background:var(--surface-3);color:var(--label-2);}
/* The idea box's own four. 'planned' borrows the accent, 'shipped' the ok
   green, and 'declined' stays deliberately quiet — a no should read as an
   answer, not as a telling-off. */
.pill.planned{background:var(--accent-soft);color:var(--accent);}
.pill.shipped{background:var(--ok-soft);color:var(--ok);}
.pill.declined{background:var(--surface-3);color:var(--label-2);}

.av{
  display:grid;place-items:center;border-radius:50%;flex:none;color:#fff;font-weight:600;
  background:hsl(var(--h) 58% 45%);letter-spacing:0;
}
:root[data-theme="dark"] .av,:root[data-theme="dark"] .av{
  background:hsl(var(--h) 56% 58%);color:#0A0A0C;
}
@media (prefers-color-scheme:light){
  :root:not([data-theme="dark"]) .av{background:hsl(var(--h) 58% 45%);color:#fff;}
}
.av.s{width:30px;height:30px;font-size:.7rem;}
.av.m{width:38px;height:38px;font-size:.8rem;}
.av.l{width:56px;height:56px;font-size:1.1rem;}
.av-wrap{position:relative;flex:none;}
.dot-on{
  position:absolute;inset-block-end:0;inset-inline-end:0;width:11px;height:11px;
  border-radius:50%;background:var(--ok);border:2px solid var(--surface);
}

.card{background:var(--surface);border-radius:var(--r-l);box-shadow:var(--sh-1);overflow:hidden;}
.empty{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;
  padding:60px 26px;text-align:center;color:var(--label-2);
}
.empty h3{font-size:1.06rem;color:var(--label);}
.empty p{max-width:38ch;font-size:.94rem;}

/* A cleared queue is the good outcome, so it gets a mark that draws itself
   rather than the same grey paragraph every other empty list gets. It runs
   once on arrival and then holds — nothing loops in the middle of a shift. */
.all-clear .ac-mark{width:54px;height:54px;display:block;margin-bottom:4px;overflow:visible;}
.ac-ring{
  fill:none;stroke:var(--ok);stroke-width:2.4;opacity:.32;
  stroke-dasharray:145;stroke-dashoffset:145;
  animation:acDraw .6s var(--ease) .06s forwards;
}
.ac-tick{
  fill:none;stroke:var(--ok);stroke-width:3.4;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:40;stroke-dashoffset:40;
  animation:acDraw .4s var(--ease) .44s forwards;
}
@keyframes acDraw{to{stroke-dashoffset:0;}}
@media (prefers-reduced-motion:reduce){
  .ac-ring,.ac-tick{animation:none;stroke-dashoffset:0;}
}
.spinner{
  width:19px;height:19px;border-radius:50%;border:2px solid var(--line-2);
  border-top-color:var(--accent);animation:spin .7s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg);}}

/* ------------------------------------------------------------------ toast */
#toasts{
  position:fixed;inset-block-start:calc(var(--head-h) + 12px);inset-inline:0;z-index:80;
  display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;
}
.toast{
  display:flex;align-items:center;gap:10px;max-width:min(460px,92vw);
  padding:12px 20px;border-radius:99px;background:var(--glass);
  -webkit-backdrop-filter:saturate(180%) blur(24px);backdrop-filter:saturate(180%) blur(24px);
  box-shadow:var(--sh-3);font-size:.92rem;font-weight:500;
  animation:toastIn .42s var(--spring);pointer-events:auto;
  transition:opacity .3s,transform .3s;
}
.toast.err{color:var(--crit);}
@keyframes toastIn{from{opacity:0;transform:translateY(-14px) scale(.94);}}

/* ============================================================ APP CHROME */
#app{min-height:100dvh;}
/* The shell is exactly one viewport tall and never scrolls itself. Scrolling
   happens inside .body (pages) or .msgs (chat), so a chat never drags the
   whole page with it. */
.shell{display:grid;grid-template-columns:var(--nav-w) minmax(0,1fr);height:100dvh;overflow:hidden;}
.side{
  position:sticky;top:0;height:100dvh;display:flex;flex-direction:column;gap:1px;
  padding:12px 10px;background:var(--sidebar);border-inline-end:1px solid var(--line);
}
.brand{display:flex;align-items:center;gap:10px;padding:10px 10px 18px;min-width:0;}
.brand svg,.brand img{flex:none;display:block;}
.brand img{height:30px;width:auto;max-width:170px;object-fit:contain;border-radius:7px;}
.brand img.wide{border-radius:0;}
.brand > span{min-width:0;}
.brand b{font-family:var(--font-d);font-size:1.02rem;font-weight:600;letter-spacing:-.022em;
  display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.brand i{font-style:normal;font-size:.7rem;color:var(--label-3);display:block;
  text-transform:capitalize;}
.nav-h{font-size:.72rem;font-weight:600;color:var(--label-3);padding:16px 12px 5px;}
.nav-a{
  display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:var(--r-s);
  color:var(--label);font-size:.95rem;transition:background .16s;
}
.nav-a svg{flex:none;color:var(--accent);}
.nav-a:hover{background:var(--surface-3);}
.nav-a.on{background:var(--accent);color:var(--on-accent);font-weight:600;}
.nav-a.on svg{color:currentColor;}
.nav-a .badge{
  margin-inline-start:auto;min-width:20px;height:20px;padding:0 6px;border-radius:99px;
  background:var(--crit);color:#fff;font-size:.7rem;font-weight:600;
  display:grid;place-items:center;font-variant-numeric:tabular-nums;
}
.nav-a.on .badge{background:var(--on-accent);color:var(--accent);}
.side-foot{margin-block-start:auto;padding-top:10px;border-top:1px solid var(--line);}
/* The developer's menu is longer than a laptop screen is tall. The menu
   scrolls, and the account and shift switch stay pinned under it. */
.side{overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;}
.side > *{flex-shrink:0;}
.side-foot{position:sticky;bottom:-12px;margin-bottom:-12px;padding-bottom:12px;
  background:inherit;z-index:1;}       /* the drawer on a phone is a different colour */

.who{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--r-s);
  width:100%;text-align:start;transition:background .16s;}
.who > span:last-child{min-width:0;flex:1;}
.who:hover{background:var(--surface-3);}
.who .nm{display:block;font-size:.9rem;font-weight:600;line-height:1.25;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.who .rl{display:block;font-size:.78rem;color:var(--label-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

.main{min-width:0;display:flex;flex-direction:column;height:100dvh;overflow:hidden;}
.head{
  position:relative;z-index:30;flex:none;display:flex;align-items:center;gap:12px;
  height:var(--head-h);padding-inline:22px;background:var(--glass);
  -webkit-backdrop-filter:saturate(180%) blur(24px);backdrop-filter:saturate(180%) blur(24px);
  border-bottom:1px solid var(--glass-line);
}
@supports not (backdrop-filter:blur(4px)){ .head{background:var(--surface);} }
.head h1{font-size:1.06rem;font-weight:600;letter-spacing:-.019em;}
.head .sub{font-size:.79rem;color:var(--label-3);}
.head-r{margin-inline-start:auto;display:flex;align-items:center;gap:8px;}
.icon-btn{
  width:34px;height:34px;border-radius:50%;display:grid;place-items:center;color:var(--accent);
  transition:background .16s,transform .2s var(--spring);position:relative;
}
.icon-btn:hover{background:var(--surface-3);}
.icon-btn:active{transform:scale(.9);}
.icon-btn .dot{
  position:absolute;top:5px;inset-inline-end:5px;width:8px;height:8px;border-radius:50%;
  background:var(--crit);border:1.5px solid var(--glass);
}
.body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;}
.body:has(.chat){overflow:hidden;}
.pad{padding:26px 24px 46px;max-width:1140px;}
.burger{display:none;}

/* ------------------------------------------------------------ mobile nav */
.tabbar{display:none;}
@media (max-width:900px){
  .shell{grid-template-columns:minmax(0,1fr);}
  .side{
    position:fixed;inset-block:0;inset-inline-start:0;width:min(300px,85vw);z-index:60;
    transform:translateX(-103%);transition:transform .34s var(--ease);
    box-shadow:var(--sh-3);background:var(--surface);
  }
  /* inset-inline-start already resolves to the right edge in rtl; only the
     slide direction flips. .show repeats the selector to outrank it. */
  html[dir="rtl"] .side{transform:translateX(103%);}
  .side.show,html[dir="rtl"] .side.show{transform:translateX(0);}
  .scrim{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:55;
    -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);}
  .burger{display:grid;}
  .pad{padding:20px 16px 96px;}
  .tabbar{
    position:fixed;inset-block-end:0;inset-inline:0;z-index:40;display:grid;
    grid-auto-flow:column;grid-auto-columns:1fr;
    padding:7px 6px calc(7px + env(safe-area-inset-bottom));
    background:var(--glass);-webkit-backdrop-filter:saturate(180%) blur(24px);
    backdrop-filter:saturate(180%) blur(24px);border-top:1px solid var(--glass-line);
  }
  .tabbar.hide{display:none;}
  .tab{
    display:flex;flex-direction:column;align-items:center;gap:3px;padding:5px 2px;
    color:var(--label-3);font-size:.63rem;font-weight:500;position:relative;
  }
  .tab.on{color:var(--accent);}
  .tab .badge{
    position:absolute;top:0;inset-inline-end:calc(50% - 18px);min-width:16px;height:16px;
    padding:0 4px;border-radius:99px;background:var(--crit);color:#fff;font-size:.6rem;
    font-weight:700;display:grid;place-items:center;
  }
}

/* ================================================================== CHAT */
.chat{display:grid;grid-template-columns:352px minmax(0,1fr);
  grid-template-rows:100%;height:100%;min-height:0;overflow:hidden;}
.threads{
  border-inline-end:1px solid var(--line);background:var(--sidebar);
  display:flex;flex-direction:column;min-height:0;min-width:0;
}
.threads-top{padding:12px 14px 11px;border-bottom:1px solid var(--line);
  display:flex;flex-direction:column;gap:11px;}
.search{
  display:flex;align-items:center;gap:7px;background:var(--surface-3);border-radius:10px;
  padding:8px 12px;color:var(--label-3);
}
.search input{background:none;border:0;outline:none;flex:1;font-size:.94rem;min-width:0;color:var(--label);}
.filters{display:flex;gap:7px;overflow-x:auto;padding-bottom:2px;scrollbar-width:none;}
.filters::-webkit-scrollbar{display:none;}
.thread-list{flex:1;overflow-y:auto;overscroll-behavior:contain;min-height:0;}
.trow{
  position:relative;display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:12px;
  width:100%;text-align:start;padding:12px 14px;transition:background .14s;
}
.trow:not(:last-child)::after{
  content:"";position:absolute;inset-block-end:0;inset-inline-start:64px;inset-inline-end:0;
  height:1px;background:var(--line);
}
.trow:hover{background:var(--surface-3);}
.trow.on{background:var(--accent);color:var(--on-accent);}
.trow.on::after{display:none;}
.trow .nm{display:block;font-size:.94rem;font-weight:600;letter-spacing:-.014em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.trow .ti{display:block;font-size:.87rem;color:var(--label-2);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.trow .sn{display:block;font-size:.83rem;color:var(--label-3);margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.trow.on .ti,.trow.on .sn{color:color-mix(in srgb,var(--on-accent) 78%,transparent);}
.trow .meta{display:flex;flex-direction:column;align-items:flex-end;gap:6px;}
.trow .tm{direction:ltr;font-size:.76rem;color:var(--label-3);white-space:nowrap;
  font-variant-numeric:tabular-nums;}
.trow.on .tm{color:color-mix(in srgb,var(--on-accent) 78%,transparent);}
.trow .badge{
  min-width:20px;height:20px;padding:0 6px;border-radius:99px;background:var(--crit);color:#fff;
  font-size:.72rem;font-weight:600;display:grid;place-items:center;font-variant-numeric:tabular-nums;
}
.trow .tags{display:flex;gap:5px;margin-top:6px;flex-wrap:wrap;}
.tag{font-size:.72rem;padding:2px 8px;border-radius:99px;background:var(--surface-3);
  color:var(--label-2);font-weight:500;}
.trow.on .tag{background:color-mix(in srgb,var(--on-accent) 22%,transparent);color:var(--on-accent);}

/* min-height:0 is essential: a grid item defaults to min-height:auto and would
   grow to fit the whole message list, pushing the composer off screen. */
.conv{display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--bg);}
.conv-head{
  flex:none;display:flex;align-items:center;gap:11px;padding:9px 18px;min-height:var(--head-h);
  background:var(--glass);-webkit-backdrop-filter:saturate(180%) blur(24px);
  backdrop-filter:saturate(180%) blur(24px);border-bottom:1px solid var(--glass-line);
}
.conv-head .who-t{font-size:.97rem;font-weight:600;line-height:1.25;letter-spacing:-.016em;}
.conv-head .sub-t{direction:ltr;text-align:start;font-size:.78rem;color:var(--label-3);margin-top:1px;}
.conv-acts{margin-inline-start:auto;display:flex;gap:7px;align-items:center;}
.msgs{
  flex:1;overflow-y:auto;overscroll-behavior:contain;min-height:0;padding:20px 20px 10px;
  display:flex;flex-direction:column;gap:3px;scroll-behavior:smooth;
}
.day{align-self:center;unicode-bidi:plaintext;font-size:.75rem;font-weight:500;
  color:var(--label-3);padding:6px 12px;margin:10px 0 6px;}
.sys{align-self:center;text-align:center;max-width:80%;font-size:.79rem;
  color:var(--label-3);padding:7px 14px;margin:5px 0;}
.msg{display:flex;flex-direction:column;gap:2px;max-width:min(74%,600px);margin-top:6px;}
.msg.in{align-self:flex-start;align-items:flex-start;}
.msg.out{align-self:flex-end;align-items:flex-end;}
.msg .who-s{font-size:.76rem;color:var(--label-3);padding-inline:12px;font-weight:500;margin-bottom:2px;}
.bub{
  text-align:start;padding:9px 15px;border-radius:20px;font-size:1rem;line-height:1.42;
  background:var(--surface-3);color:var(--label);overflow-wrap:anywhere;
}
.msg.out .bub{background:var(--accent);color:var(--on-accent);}
.bub p + p{margin-top:8px;}
.rcpt{
  direction:ltr;font-size:.72rem;color:var(--label-3);padding-inline:12px;margin-top:2px;
  display:flex;align-items:center;gap:4px;font-variant-numeric:tabular-nums;
}
.rcpt .tick{color:var(--accent);font-size:.82rem;letter-spacing:-2px;}
/* The row under a message carries its actions. They stay quiet until the
   message is hovered, and are always there on a touch screen, which has no
   hover to reveal them with. */
.rcpt .mrep,.rcpt .mdel,.rcpt .mreact{
  display:grid;place-items:center;width:22px;height:22px;border-radius:50%;
  color:var(--label-3);opacity:0;transition:opacity .16s var(--ease),background .16s var(--ease);
}
.rcpt .mrep:hover,.rcpt .mdel:hover,.rcpt .mreact:hover{background:var(--surface-3);color:var(--label-1);}
.msg:hover .rcpt .mrep,.msg:hover .rcpt .mdel,.msg:hover .rcpt .mreact,
.rcpt .mrep:focus-visible,.rcpt .mdel:focus-visible,.rcpt .mreact:focus-visible{opacity:1;}
@media (hover:none){ .rcpt .mrep,.rcpt .mdel,.rcpt .mreact{opacity:.65;} }
.msg.sending{opacity:.5;}
.msg.failed .bub{outline:1.5px solid var(--crit);}

.bub.media{padding:4px;border-radius:20px;}
.bub.media img{
  display:block;max-width:min(320px,62vw);width:100%;height:auto;border-radius:17px;
  cursor:zoom-in;background:var(--surface-4);
}
.bub.media .cap{padding:8px 11px 5px;font-size:.97rem;}

/* A video sits in the same bubble a photograph does and takes the same width,
   so a conversation with both in it does not step in and out. The height is
   the file's own — letting it stretch to a fixed box would letterbox a clip
   somebody shot in portrait, which on this platform is most of them. */
.bub.media.vid{overflow:hidden;}   /* the player's own corners are square */
.bub.media.vid video{
  display:block;max-width:min(320px,62vw);width:100%;height:auto;max-height:60vh;
  border-radius:17px;background:#000;
}

/* ---------------------------------------------------------------- replies
   The quote sits inside the reply's own bubble and is a way back to what was
   answered. It borrows the bubble's colour rather than setting its own, so it
   reads correctly on the accent of a sent message and on the surface of a
   received one without a second set of tokens. */
.quote{
  display:flex;flex-direction:column;gap:1px;width:100%;text-align:start;
  padding:6px 10px;margin-bottom:6px;border-radius:12px;
  border-inline-start:3px solid currentColor;
  background:color-mix(in srgb,currentColor 12%,transparent);
  color:inherit;font-size:.86rem;line-height:1.35;cursor:pointer;
  transition:background .16s var(--ease);
}
.quote:hover{background:color-mix(in srgb,currentColor 20%,transparent);}
.quote:focus-visible{outline:2px solid currentColor;outline-offset:1px;}
.quote .q-who{font-weight:700;opacity:.95;}
.quote .q-body{
  opacity:.8;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden;overflow-wrap:anywhere;
}
.quote.gone{cursor:default;font-style:italic;opacity:.7;display:block;}
.bub.media .quote{margin:4px 4px 6px;width:auto;}

/* Where the eye lands after tapping a quote. */
@keyframes msgFlash{
  from{background:color-mix(in srgb,var(--accent) 30%,transparent);}
  to{background:transparent;}
}
.msg.flash .bub{animation:msgFlash 1.5s var(--ease);}

/* -------------------------------------------------------------- reactions
   The chips sit under the bubble, on the message's own side of the column, so
   a reaction reads as belonging to the line above it rather than to the one
   below. They carry their own colour rather than the bubble's: a chip on a
   sent message would otherwise be accent-on-accent and unreadable. */
.reacts{
  display:flex; flex-wrap:wrap; gap:4px; margin-top:-3px;
  padding-inline:4px; max-width:100%;
}
.react{
  display:inline-flex; align-items:center; gap:4px;
  padding:2px 8px 2px 6px; border-radius:999px;
  background:var(--surface-3); border:1px solid transparent;
  font-size:.8rem; line-height:1.5; cursor:pointer;
  transition:background .16s var(--ease), border-color .16s var(--ease), transform .16s var(--spring);
}
.react:hover{ background:var(--surface-4); }
.react:active{ transform:scale(.92); }
.react.mine{ background:color-mix(in srgb,var(--accent) 16%,transparent); border-color:var(--accent); }
.react:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.react .e{ font-size:.95rem; line-height:1; }
.react .n{ font-variant-numeric:tabular-nums; font-weight:600; color:var(--label-2); }
.react.mine .n{ color:var(--accent); }

/* Who is behind a chip, on hover and on keyboard focus. One element for the
   whole page, moved to whichever chip is being pointed at.

   It sits in the empty column beside the message rather than over it, so it
   reads as a note in the margin — which is what it is. A near-black slab was
   the first attempt and it landed on top of the conversation; this is the
   surface the app already uses for raised things, aligned left because a name
   list is read, not centred. */
.react-who{
  position:fixed; z-index:97;
  padding:5px 9px; border-radius:8px;
  background:var(--surface); color:var(--label-2);
  border:1px solid var(--line);
  font-size:.75rem; font-weight:500; line-height:1.45; text-align:start;
  box-shadow:var(--sh-2); pointer-events:none;
  animation:whoIn .13s var(--ease) both;
}
/* It slides in from the side it is standing on. */
@keyframes whoIn{ from{ opacity:0; transform:translateX(-4px); } }
.react-who.to-start{ animation-name:whoInStart; }
@keyframes whoInStart{ from{ opacity:0; transform:translateX(4px); } }

/* Only the chip that was actually just touched — the class is put on one chip
   at render time and spent immediately, so an unrelated redraw animates
   nothing. The emoji overshoots and settles; the chip itself only fades up,
   because two things springing at once reads as a glitch. */
@keyframes reactIn{
  from{ opacity:0; transform:translateY(3px) scale(.7); }
  to{ opacity:1; transform:none; }
}
@keyframes reactBounce{
  0%{ transform:scale(.5); }
  55%{ transform:scale(1.35); }
  100%{ transform:scale(1); }
}
.react.pop{ animation:reactIn .22s var(--ease) both; }
.react.pop .e{ animation:reactBounce .34s var(--spring) both; display:inline-block; }

/* The picker. Fixed to the window rather than to the message, because the
   message list scrolls and a menu that scrolls away under the thumb is worse
   than no menu. */
.react-pop{
  position:fixed; z-index:96; width:min(324px, calc(100vw - 16px));
  display:flex; flex-direction:column; gap:7px; padding:8px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r-l);
  box-shadow:var(--sh-3); animation:popIn .16s var(--spring);
}
/* The row that answers most taps without anyone having to look further. */
.react-pop .rp-quick{
  display:flex; gap:2px; flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none;
  padding-bottom:1px; border-bottom:1px solid var(--line);
}
.react-pop .rp-quick::-webkit-scrollbar{ display:none; }
.react-pop .rp-find{
  display:flex; align-items:center; gap:7px; padding:5px 10px;
  background:var(--surface-2); border-radius:999px; color:var(--label-3);
}
.react-pop .rp-find input{
  flex:1; min-width:0; border:0; background:none; outline:none;
  font:inherit; font-size:.86rem; color:var(--label);
}
.react-pop .rp-grid{ max-height:206px; overflow-y:auto; overscroll-behavior:contain; }
.react-pop .rp-cat + .rp-cat{ margin-top:6px; }
.react-pop .rp-cat h4{
  position:sticky; top:0; z-index:1; margin:0 0 2px; padding:3px 4px;
  background:var(--surface); font-size:.68rem; font-weight:600;
  letter-spacing:.05em; text-transform:uppercase; color:var(--label-4);
}
.react-pop .rp-row{ display:grid; grid-template-columns:repeat(8, 1fr); gap:1px; }
.react-pop .rp-e{
  aspect-ratio:1; width:100%; border-radius:8px; font-size:1.16rem; line-height:1;
  display:grid; place-items:center; cursor:pointer;
  transition:background .12s var(--ease), transform .12s var(--spring);
}
.react-pop .rp-quick .rp-e{ width:34px; aspect-ratio:auto; height:34px; border-radius:50%; flex:none; }
.react-pop .rp-e:hover{ background:var(--surface-3); transform:scale(1.18); }
.react-pop .rp-e:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
@media (max-width:420px){
  .react-pop .rp-row{ grid-template-columns:repeat(7, 1fr); }
  .react-pop .rp-grid{ max-height:172px; }
}

/* The bar above the composer while an answer is being written. */
/* The bar grows out of the top of the composer rather than appearing whole,
   so the answer being written is visibly attached to the message above it.
   Drawn only when the reply changes, so there is no redraw to replay it. */
@keyframes replyBarIn{
  from{ opacity:0; transform:translateY(6px); clip-path:inset(100% 0 0 0); }
  to{ opacity:1; transform:none; clip-path:inset(0 0 0 0); }
}
.reply-bar{
  display:flex;align-items:center;gap:10px;padding:8px 12px;
  border-top:1px solid var(--line);background:var(--surface-2);
  animation:replyBarIn .24s var(--ease) both;
}
/* The arrow leads, so the eye goes to what the bar is pointing back at. */
@keyframes replyMarkIn{ from{ opacity:0; transform:translateX(-5px); } }
.rb-mark{ animation:replyMarkIn .3s .06s var(--ease) both; }
html[dir="rtl"] .rb-mark{ animation-name:replyMarkInRtl; }
@keyframes replyMarkInRtl{ from{ opacity:0; transform:translateX(5px); } }
.rb-mark{display:grid;place-items:center;color:var(--accent);flex:none;}
.rb-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1;}
.rb-who{font-size:.76rem;font-weight:700;color:var(--accent);}
.rb-body{
  font-size:.86rem;color:var(--label-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.rb-x{width:30px;height:30px;flex:none;}

.voice{display:flex;flex-direction:column;gap:6px;min-width:220px;}
.vrow{display:flex;align-items:center;gap:12px;}
.voice .play{
  width:32px;height:32px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:color-mix(in srgb,currentColor 16%,transparent);color:inherit;
}
.wave{display:flex;align-items:center;gap:2.5px;height:26px;flex:1;min-width:70px;}
.wave i{width:3px;border-radius:99px;background:currentColor;opacity:.35;transition:opacity .1s;}
.wave i.on{opacity:1;}
html[dir="rtl"] .wave{flex-direction:row-reverse;}
.vtime{direction:ltr;font-size:.78rem;opacity:.7;font-variant-numeric:tabular-nums;}

.katex,.katex-display{direction:ltr;unicode-bidi:isolate;}
.katex{font-size:1.03em;}
.katex-display{margin:.5em 0;overflow-x:auto;overflow-y:hidden;padding-bottom:2px;}
.msg.out .katex,.msg.out .katex *{color:inherit;}
.mathfail{color:var(--crit);font-family:var(--font-m);font-size:.85em;}
/* Maths in the moment between a body being drawn and KaTeX arriving to set
   it. Pointedly not .mathfail red — nothing has failed, and a physics
   student reads $v=at$ perfectly well on its own. */
.math-pending{font-family:var(--font-m);font-size:.95em;opacity:.72;}

.composer{
  flex:none;border-top:1px solid var(--line);background:var(--glass);
  -webkit-backdrop-filter:saturate(180%) blur(24px);backdrop-filter:saturate(180%) blur(24px);
  padding:10px 16px 14px;
}
.crow{display:flex;align-items:flex-end;gap:9px;}
.cinput{
  flex:1;min-width:0;background:var(--surface);border:1px solid var(--line-2);
  border-radius:20px;padding:8px 16px;display:flex;align-items:flex-end;
}
.cinput textarea{
  width:100%;background:none;border:0;outline:none;resize:none;font-size:1rem;
  line-height:1.45;max-height:150px;min-height:23px;
}
.cbtn{
  width:34px;height:34px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:transparent;color:var(--accent);transition:background .16s,transform .2s var(--spring);
}
.cbtn:hover{background:var(--surface-3);}
.cbtn:active{transform:scale(.88);}
.cbtn.send{background:var(--accent);color:var(--on-accent);}
.cbtn.rec{background:var(--crit);color:#fff;animation:pulse 1.4s ease-in-out infinite;}
@keyframes pulse{50%{opacity:.55;}}
.chint{font-size:.74rem;color:var(--label-3);padding:8px 6px 0;display:flex;gap:16px;flex-wrap:wrap;}
.preview-bar{
  display:flex;align-items:center;gap:11px;padding:9px 11px;margin-bottom:10px;
  background:var(--surface-3);border-radius:var(--r-m);font-size:.88rem;
}
.preview-bar img,.preview-bar video{
  width:44px;height:44px;object-fit:cover;border-radius:9px;background:#000;
}
.typing{padding:2px 22px 6px;font-size:.8rem;color:var(--label-3);height:22px;}
.reclive{
  display:flex;align-items:center;gap:10px;padding:10px 16px;background:var(--crit-soft);
  color:var(--crit);border-radius:20px;flex:1;font-size:.92rem;font-weight:500;
}
.reclive .rdot{width:9px;height:9px;border-radius:50%;background:var(--crit);animation:pulse 1s infinite;}

@media (max-width:900px){
  .chat{grid-template-columns:minmax(0,1fr);grid-template-rows:100%;height:100%;}
  .chat[data-pane="conv"] .threads{display:none;}
  .chat[data-pane="list"] .conv{display:none;}
  .msg{max-width:86%;}
}

/* =============================================================== LANDING */
body:has(.hero),body:has(.auth-wrap){overflow-y:auto;}
.hero{
  position:relative;padding:clamp(40px,8vw,90px) clamp(20px,6vw,64px) clamp(56px,9vw,110px);
  max-width:1140px;margin:0 auto;text-align:center;
  min-height:calc(100dvh - var(--head-h));
  display:flex;flex-direction:column;align-items:center;justify-content:center;
}
.hero .kicker{
  display:inline-flex;align-items:center;gap:8px;font-size:.86rem;font-weight:600;
  color:var(--accent);margin-bottom:20px;
}
.hero h1{font-size:clamp(2.6rem,7vw,4.6rem);letter-spacing:-.04em;line-height:1.04;
  max-width:15ch;margin-inline:auto;font-weight:700;}
.hero h1 em{font-style:normal;color:var(--accent);}
.hero .lede{font-size:clamp(1.1rem,2.1vw,1.4rem);color:var(--label-2);max-width:50ch;
  margin:22px auto 0;line-height:1.45;letter-spacing:-.014em;}
.hero .cta{display:flex;gap:12px;margin-top:34px;flex-wrap:wrap;justify-content:center;}
.hero .under{margin-top:18px;font-size:.87rem;color:var(--label-3);}
/* Studio credit, bottom-right of every page. Anchored to the physical
   bottom-right so it stays put in both reading directions, and it lets every
   tap through. A chat shows it on its own line under the composer instead
   (see .chat-credit), and the meeting in its own layout (.live-credit). */
.credit{display:none;}
body[data-credit="on"] .credit{
  display:block;
  position:fixed;bottom:10px;right:14px;z-index:35;
  font-size:.68rem;font-weight:500;letter-spacing:.02em;color:var(--label-4);
  pointer-events:none;user-select:none;opacity:.85;
  animation:fadeIn .8s var(--ease) both .4s;
}
/* Sits above the mobile tab bar rather than under it. */
@media (max-width:900px){
  body[data-credit="on"] .credit{right:12px;font-size:.64rem;bottom:calc(10px + env(safe-area-inset-bottom));}
  body[data-credit="on"]:has(.tabbar:not(.hide)) .credit{bottom:calc(58px + env(safe-area-inset-bottom));}
}
/* A chat has its own line for it, under the box you type in. */
body:has(.chat) .credit{display:none !important;}

/* ================================================================== AUTH */
.auth-wrap{min-height:100dvh;display:grid;place-items:center;padding:32px 20px 60px;}
.auth{width:min(440px,100%);}
.auth-card{background:var(--surface);border-radius:var(--r-xl);box-shadow:var(--sh-2);
  padding:clamp(26px,4vw,38px);}
.auth h1{font-size:1.72rem;margin-bottom:7px;letter-spacing:-.028em;}
.auth .sub{color:var(--label-2);font-size:.98rem;margin-bottom:26px;}
.auth form{display:flex;flex-direction:column;gap:16px;}
.two{display:grid;grid-template-columns:1fr 1fr;gap:13px;}
@media (max-width:520px){ .two{grid-template-columns:1fr;} }
.auth-foot{text-align:center;margin-top:20px;font-size:.93rem;color:var(--label-2);}
.demo{margin-top:22px;padding:16px 18px;border-radius:var(--r-m);background:var(--surface-2);}
:root[data-theme="dark"] .demo,:root[data-theme="dark"] .demo{background:var(--surface-3);}
.demo h4{font-size:.75rem;font-weight:600;color:var(--label-3);margin-bottom:10px;}
.demo-row{display:flex;align-items:center;gap:10px;padding:5px 0;font-size:.87rem;}
.demo-row .r{font-size:.79rem;color:var(--label-3);width:66px;flex:none;}
.demo-row button{color:var(--accent);font-size:.87rem;font-weight:500;}

/* ============================================================ DASHBOARD */
.grid{display:grid;gap:13px;}
.g3{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));}
.g2{grid-template-columns:repeat(auto-fit,minmax(300px,1fr));}
.tile{padding:20px 22px;border-radius:var(--r-l);background:var(--surface);
  transition:transform .22s var(--spring);}
.tile.link:active{transform:scale(.985);}
.tile .k{font-size:.83rem;font-weight:500;color:var(--label-2);}
.tile .v{font-family:var(--font-d);font-size:2.15rem;font-weight:700;letter-spacing:-.035em;
  margin-top:6px;font-variant-numeric:tabular-nums;line-height:1.05;}
.tile .d{font-size:.85rem;color:var(--label-3);margin-top:4px;}
.sec-h{display:flex;align-items:center;gap:12px;margin:32px 0 12px;padding-inline:4px;}
.sec-h h2{font-size:1.24rem;letter-spacing:-.022em;}
.sec-h .more{margin-inline-start:auto;font-size:.92rem;color:var(--accent);font-weight:500;}
.list{display:flex;flex-direction:column;}
.lrow{position:relative;display:flex;align-items:center;gap:12px;padding:13px 18px;
  width:100%;text-align:start;transition:background .14s;}
.list > *:not(:last-child)::after{
  content:"";position:absolute;inset-block-end:0;inset-inline-start:58px;inset-inline-end:0;
  height:1px;background:var(--line);
}
.lrow:hover{background:var(--surface-3);}
.lrow .t{display:block;font-size:.96rem;font-weight:500;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.lrow .s{display:block;font-size:.84rem;color:var(--label-3);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.lrow .r{margin-inline-start:auto;display:flex;align-items:center;gap:9px;flex:none;}

/* The approvals queue, explaining itself. Why somebody is still here reads as
   a quiet note; what the register nearly says is offered as something to press,
   because it fills the number in rather than deciding anything. */
.appr-top{display:flex;align-items:center;gap:12px;margin:0 2px 14px;flex-wrap:wrap;}
.appr-top .hint{flex:1 1 320px;}
.lrow .s.held{color:var(--warn);white-space:normal;}
.lrow .s.near{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px;white-space:normal;}
.nearbtn{font:inherit;font-size:.8rem;color:var(--label-2);cursor:pointer;
  background:var(--surface-3);border:1px solid var(--line);border-radius:6px;
  padding:3px 8px;display:inline-flex;align-items:center;gap:6px;max-width:100%;}
.nearbtn:hover{border-color:var(--accent);color:var(--label-1);}
.nearbtn b{font-size:.78rem;}
.nearbtn i{font-style:normal;color:var(--label-3);}
.appr-id.filled{border-color:var(--accent);}

/* The name, editable in place. It has to keep reading as the row's title —
   this is a list of people, and a row of form fields is not a list of people —
   so it carries no chrome until it is pointed at, and only then admits to
   being an input. A nickname is repaired here, so it is wide and plain. */
/* An input is a replaced element: width:auto keeps its intrinsic size rather
   than filling, so the padding that makes room for the focus ring has to be
   paid back explicitly or the field hangs 12px past the row. */
.appr-name{display:block;width:calc(100% + 12px);margin:-2px -6px 0;
  font:inherit;font-size:.96rem;font-weight:500;
  color:var(--label-1);background:transparent;border:1px solid transparent;
  border-radius:6px;padding:2px 6px;
  overflow:hidden;text-overflow:ellipsis;
  transition:background .16s var(--ease),border-color .16s var(--ease);}
.appr-name:hover{border-color:var(--line);}
.appr-name:focus{outline:none;background:var(--surface-1);border-color:var(--accent);}
.appr-name.filled{border-color:var(--accent);}

/* The log. An automatic approval is the one line in there nobody signed, so
   it is the one line that says so plainly. The number beside it is what the
   team looks a student up by, so it is set apart from the timestamp. */
.audit-auto{letter-spacing:.04em;font-weight:650;color:var(--ok);}
.audit-sid{font-size:.78rem;padding:2px 7px;border-radius:5px;
  background:var(--surface-3);color:var(--label-2);}

.tbl{width:100%;border-collapse:collapse;font-size:.92rem;}
.tbl th{text-align:start;font-size:.78rem;color:var(--label-3);padding:12px 18px;
  border-bottom:1px solid var(--line);white-space:nowrap;font-weight:500;}
.tbl td{padding:13px 18px;border-bottom:1px solid var(--line);vertical-align:middle;
  font-variant-numeric:tabular-nums;}
.tbl tr:last-child td{border-bottom:0;}
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%;}
.bar{height:6px;border-radius:99px;background:var(--surface-3);overflow:hidden;min-width:60px;}
.bar i{display:block;height:100%;background:var(--accent);border-radius:99px;}

/* On a phone a wide table becomes one card per person: no sideways scrolling,
   every field labelled, and the actions full width. Opt-in via .tbl.cards so
   other tables keep their normal layout. */
@media (max-width:720px){
  .tbl-wrap:has(.tbl.cards){overflow:visible;}
  .tbl.cards,.tbl.cards tbody,.tbl.cards tr,.tbl.cards td{display:block;width:100%;}
  .tbl.cards thead{display:none;}
  .tbl.cards tr{
    padding:14px 16px;border-bottom:1px solid var(--line);
  }
  .tbl.cards tr:last-child{border-bottom:0;}
  .tbl.cards td{
    display:flex;align-items:center;justify-content:space-between;gap:14px;
    padding:5px 0;border:0;white-space:normal;font-size:.9rem;
  }
  .tbl.cards td[data-label]::before{
    content:attr(data-label);color:var(--label-3);font-size:.82rem;flex:none;
  }
  /* The name is the card's heading, not a labelled field. */
  .tbl.cards td[data-label="Name"]{
    display:block;padding:0 0 10px;margin-bottom:8px;border-bottom:1px solid var(--line);
  }
  .tbl.cards td[data-label="Name"]::before{content:none;}
  .tbl.cards td[data-label="Role"]{justify-content:flex-start;}
  .tbl.cards td.row-actions{padding-top:12px;}
  .tbl.cards .act-wrap{display:grid;grid-template-columns:1fr 1fr;gap:8px;width:100%;}
  .tbl.cards .act-wrap .btn{width:100%;height:38px;font-size:.86rem;}
  .tbl.cards .act-wrap .btn.danger{grid-column:1 / -1;}
  .tbl.cards td .empty{padding:34px 10px;}
}
.act-wrap{display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap;}
.row-actions{text-align:end;}
.sort-dir{min-width:42px;padding:0 12px;font-size:1rem;}

.title-edit{display:flex;align-items:center;gap:8px;border-radius:9px;padding:2px 8px 2px 2px;
  color:inherit;transition:background .16s;}
.title-edit:hover{background:var(--surface-3);}
.title-edit svg{color:var(--label-3);opacity:0;transition:opacity .16s;}
.title-edit:hover svg,.title-edit:focus-visible svg{opacity:1;}
@media (pointer:coarse){ .title-edit svg{opacity:1;} }

/* A real checkbox, styled — keeps keyboard and screen-reader behaviour. */
.check{display:flex;align-items:flex-start;gap:12px;cursor:pointer;padding:13px 15px;
  border-radius:var(--r-m);background:var(--surface-3);transition:background .16s;}
.check:hover{background:var(--surface-4);}
.check input{position:absolute;opacity:0;width:0;height:0;}
.check .box{
  flex:none;width:24px;height:24px;border-radius:7px;background:var(--surface);
  border:1.5px solid var(--line-2);display:grid;place-items:center;margin-top:1px;
  transition:background .18s var(--ease),border-color .18s;
}
.check .box::after{
  content:"";width:11px;height:6px;border-inline-start:2.5px solid var(--on-accent);
  border-block-end:2.5px solid var(--on-accent);transform:rotate(-45deg) scale(.4);
  opacity:0;transition:opacity .16s,transform .22s var(--spring);margin-top:-3px;
}
.check input:checked ~ .box{background:var(--accent);border-color:var(--accent);}
.check input:checked ~ .box::after{opacity:1;transform:rotate(-45deg) scale(1);}
.check input:focus-visible ~ .box{outline:3px solid var(--accent-line);outline-offset:2px;}
.check-title{display:block;font-weight:500;}

.name-cell{display:flex;align-items:center;gap:10px;text-align:start;width:100%;
  border-radius:8px;padding:2px;transition:background .14s;}
.name-cell:hover{background:var(--surface-3);}
.callout-ok{padding:13px 15px;border-radius:var(--r-m);background:var(--ok-soft);color:var(--ok);
  font-size:.92rem;border-inline-start:3px solid var(--ok);}
.callout-ok p{color:var(--label-2);}

/* Roster filter row + sortable headers. */
.filters-bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}
.sortbtn{
  font:inherit;color:inherit;display:inline-flex;align-items:center;gap:2px;
  padding:0;border-radius:5px;white-space:nowrap;
}
.sortbtn:hover{color:var(--label);}
.sortbtn.on{color:var(--accent);font-weight:600;}
.tbl th:has(.sortbtn){cursor:pointer;}
.tbl td a[href^="tel:"]{color:var(--accent);}

/* The student's number, edited in place. It reads as text until you go near it,
   so a roster of numbers does not look like a wall of buttons — but the one
   that is missing says so, because filling it in is the whole point. */
.idcell{
  font:inherit;color:inherit;padding:3px 7px;margin:-3px -7px;border-radius:6px;
  border:1px solid transparent;white-space:nowrap;
  transition:background .16s var(--ease),border-color .16s var(--ease),color .16s;
}
.idcell:hover,.idcell:focus-visible{background:var(--surface-3);border-color:var(--line);}
.idcell.unset{color:var(--label-4);font-style:italic;}
.idcell.unset:hover,.idcell.unset:focus-visible{color:var(--accent);}

/* Approvals: the number sits beside the decision, because the person deciding
   is the one holding the card it is printed on. */
.appr-r{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end;}
.appr-id{width:104px;padding:7px 9px;font-size:.86rem;text-align:center;}
@media (max-width:720px){
  .appr-r{width:100%;justify-content:stretch;}
  .appr-id{flex:1;width:auto;}
}
@media (max-width:720px){
  .filters-bar{gap:8px;}
  .filters-bar .select{max-width:none !important;flex:1;min-width:140px;}
}

/* tel: links look and behave like buttons. */
a.btn{text-decoration:none;}
a.btn:hover{border-bottom:0;}

/* A destructive action looks destructive, not like every other button. */
.btn.danger-solid{background:var(--crit);color:#fff;font-weight:600;}
.btn.danger-solid:hover{background:var(--crit);filter:brightness(1.08);}
:root[data-theme="dark"] .btn.danger-solid,
:root[data-theme="dark"] .btn.danger-solid{color:#2E1614;}
.callout-warn{
  padding:13px 15px;border-radius:var(--r-m);background:var(--crit-soft);color:var(--crit);
  font-size:.92rem;border-inline-start:3px solid var(--crit);
}
.callout-warn ul{color:var(--label-2);}

/* ============================================================== BRANDING */
.drop{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;
  padding:24px 20px;border-radius:var(--r-m);background:var(--surface-2);
  border:2px dashed var(--line-2);text-align:center;cursor:pointer;
  transition:border-color .18s,background .18s;
}
:root[data-theme="dark"] .drop,:root[data-theme="dark"] .drop{background:var(--surface-3);}
.drop:hover,.drop.over{border-color:var(--accent);background:var(--accent-soft);}
.drop .logo-prev{max-width:190px;max-height:60px;object-fit:contain;}
.drop .ph{width:50px;height:50px;border-radius:14px;background:var(--surface-4);
  display:grid;place-items:center;color:var(--label-3);}
.drop small{color:var(--label-3);font-size:.82rem;}
.drop .fname{font-size:.85rem;font-weight:500;}
.brand-row{display:grid;grid-template-columns:1fr 1fr;gap:13px;}
@media (max-width:640px){ .brand-row{grid-template-columns:1fr;} }
.swatch{width:40px;height:40px;border-radius:10px;border:1px solid var(--line-2);
  padding:0;cursor:pointer;background:none;flex:none;}
.swatch::-webkit-color-swatch-wrapper{padding:3px;}
.swatch::-webkit-color-swatch{border:none;border-radius:7px;}
.brand-preview{
  display:flex;align-items:center;gap:11px;padding:14px 16px;border-radius:var(--r-m);
  background:var(--sidebar);border:1px solid var(--line);
}

/* ============================================================== LIGHTBOX */
.lightbox{
  position:fixed;inset:0;z-index:90;background:rgba(0,0,0,.72);display:grid;place-items:center;
  padding:32px;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);
  animation:fade .26s var(--ease);
}
@keyframes fade{from{opacity:0;}}
.lightbox img{max-width:100%;max-height:100%;border-radius:14px;box-shadow:var(--sh-3);}

/* A room's two actions: go in, and — for the developer, on somebody else's
   room — close it. The destructive one is quiet and second. */
.lr-acts{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.lr-acts .lr-go{ flex:1 1 auto; }
.lr-acts .lr-end{ flex:0 0 auto; color:var(--crit); }
.lr-acts .lr-end:hover{ background:color-mix(in srgb,var(--crit) 12%,transparent); }
.btn.crit{ background:var(--crit); border-color:var(--crit); color:#fff; }
.btn.crit:hover{ filter:brightness(1.06); }

/* The score a student gave, on the conversation it belongs to. The unearned
   stars stay in place at low contrast rather than disappearing, so three out
   of five reads as three out of five and not as three. */
.pill.rated{
  background:color-mix(in srgb,var(--warn) 16%,transparent); color:var(--warn);
  letter-spacing:1px; font-size:.8rem;
}
.pill.rated .dim{ opacity:.32; }

/* Performance, per person. */
.tbl-h{
  font-size:.78rem; font-weight:600; letter-spacing:.05em; text-transform:uppercase;
  color:var(--label-3); margin:22px 2px 8px;
}
.tbl-h:first-child{ margin-top:0; }
.rated-of{ color:var(--label-4); font-size:.8rem; font-weight:500; }

/* -------------------------------------------------- the register importer
   Sits above the queue it is there to shorten. */
.roster-card{
  display:flex; align-items:center; gap:14px; flex-wrap:wrap;
  padding:14px 16px; margin-bottom:14px;
}
.roster-card .rc-t{ display:flex; flex-direction:column; gap:2px; min-width:0; flex:1; }
.roster-card .rc-t b{ font-size:.95rem; font-weight:600; }
.roster-card .rc-t .hint{ font-size:.8rem; }
.roster-card .btn{ flex:none; }

/* The two rows shown back before anything is approved. */
.roster-peek .lrow{ display:grid; grid-template-columns:minmax(90px,1fr) 2fr; gap:12px; }
.roster-peek .rp-head .t{ font-size:.72rem; letter-spacing:.05em; text-transform:uppercase;
  color:var(--label-4); font-weight:600; }

.roster-tally{ display:flex; gap:10px; flex-wrap:wrap; }
.roster-tally .rt{
  display:flex; align-items:baseline; gap:6px; padding:9px 14px; border-radius:var(--r-m);
  font-size:.84rem; font-weight:500;
}
.roster-tally .rt b{ font-size:1.35rem; font-weight:700; font-variant-numeric:tabular-nums; }
.roster-tally .rt.ok{ background:color-mix(in srgb,var(--ok) 14%,transparent); color:var(--ok); }
.roster-tally .rt.warn{ background:color-mix(in srgb,var(--warn) 16%,transparent); color:var(--warn); }

.roster-group{ border-top:1px solid var(--line); padding-top:10px; }
.roster-group summary{
  cursor:pointer; font-size:.86rem; font-weight:600; padding:4px 0;
  list-style-position:outside;
}
.roster-group summary:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.roster-group .list{ margin-top:8px; max-height:260px; overflow-y:auto; }

/* ------------------------------------------------------------ the camera
   A viewfinder that fills the screen, because a question being photographed
   wants framing. Black, chromeless, and the shutter under the thumb. */
body.cam-open{ overflow:hidden; }
.cam-view{
  position:fixed; inset:0; z-index:98; background:#000;
  display:flex; flex-direction:column; animation:fade .2s var(--ease);
}
.cam-view video{
  flex:1; min-height:0; width:100%; object-fit:contain; background:#000;
}
/* A selfie preview is mirrored the way every camera app shows it; what gets
   captured is not, which is handled in script. */
.cam-view video.mirror{ transform:scaleX(-1); }
.cam-bar{
  flex:none; display:grid; grid-template-columns:1fr auto 1fr; align-items:center;
  gap:18px; padding:18px 24px calc(18px + env(safe-area-inset-bottom));
  background:#000;
}
.cam-bar button{ color:#fff; cursor:pointer; }
.cam-bar .cam-x{ justify-self:start; width:44px; height:44px; border-radius:50%;
  display:grid; place-items:center; background:rgba(255,255,255,.12); }
.cam-bar .cam-flip{ justify-self:end; width:44px; height:44px; border-radius:50%;
  display:grid; place-items:center; background:rgba(255,255,255,.12); }
.cam-bar .cam-x:hover,.cam-bar .cam-flip:hover{ background:rgba(255,255,255,.22); }
/* The shutter: a ring with a filled centre, the shape everyone already knows. */
.cam-bar .cam-shot{
  width:70px; height:70px; border-radius:50%; padding:5px;
  border:3px solid #fff; background:transparent; display:grid; place-items:center;
  transition:transform .12s var(--spring);
}
.cam-bar .cam-shot span{ display:block; width:100%; height:100%; border-radius:50%; background:#fff; }
.cam-bar .cam-shot:active{ transform:scale(.9); }
.cam-bar button:focus-visible{ outline:2px solid #fff; outline-offset:3px; }
@media (max-height:520px){
  .cam-bar{ padding:10px 18px calc(10px + env(safe-area-inset-bottom)); }
  .cam-bar .cam-shot{ width:56px; height:56px; }
}

/* ------------------------------------------------------------- the photo
   A picture in a thread is usually a page of working, so it has to be
   readable: it zooms, and only the backdrop closes it. */
body.photo-open{overflow:hidden;}
.photo-view{padding:0;z-index:95;background:rgba(0,0,0,.88);display:block;}
.pv-stage{
  position:absolute;inset:0;display:grid;place-items:center;
  overflow:hidden;touch-action:none;
}
/* Sized by fit() in openPhoto, and centred by position rather than by the grid:
   a picture turned sideways is wider than the stage before it is rotated, and
   a grid track would grow to hold it and push it off centre. Hidden until it
   has a size, so it never flashes up at full resolution first. */
.pv-stage img{
  position:absolute;left:50%;top:50%;
  max-width:none;max-height:none;border-radius:0;box-shadow:none;
  transform-origin:center center;will-change:transform;
  -webkit-user-select:none;user-select:none;cursor:zoom-in;
  visibility:hidden;
}
.pv-stage img.fit{visibility:visible;}
.photo-view.turning .pv-stage img{
  transition:transform .3s var(--ease),width .3s var(--ease),height .3s var(--ease);
}
.photo-view.zoomed .pv-stage img{cursor:grab;}
.photo-view.zoomed .pv-stage img:active{cursor:grabbing;}
.pv-acts{
  position:absolute;z-index:2;top:max(10px,env(safe-area-inset-top));inset-inline-end:10px;
  display:flex;align-items:center;gap:3px;padding:4px;
  background:rgba(22,26,33,.72);border-radius:999px;
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.pv-acts .icon-btn{width:34px;height:34px;color:#fff;}
.pv-acts .icon-btn:hover{background:rgba(255,255,255,.16);}
.pv-acts .pv-pct{
  width:auto;min-width:52px;padding:0 8px;border-radius:999px;
  font-size:.76rem;font-weight:600;font-variant-numeric:tabular-nums;
}
.pv-acts .pv-x{margin-inline-start:2px;}
@media (max-width:640px){
  /* Under the thumb rather than up at the top corner. */
  .pv-acts{top:auto;inset-block-end:max(14px,env(safe-area-inset-bottom));
    inset-inline:auto;left:50%;transform:translateX(-50%);}
}
#sheet .card{animation:sheetIn .4s var(--spring);}
/* A fixed header over a body that alone scrolls. dvh so a phone's URL bar
   cannot push the last row off screen; contain so reaching the end of the
   list does not start scrolling the page behind the sheet. */
.sheet-card{width:min(520px,94vw);max-height:82vh;max-height:82dvh;
  display:flex;flex-direction:column;box-shadow:var(--sh-3);}
.sheet-h{display:flex;align-items:center;gap:10px;padding:15px 18px;flex:none;
  border-bottom:1px solid var(--line);background:var(--surface);}
.sheet-b{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;}
@keyframes sheetIn{from{opacity:0;transform:translateY(20px) scale(.97);}}

/* ================================================================ MOTION
   Short, spring-weighted, and only where it explains what changed. */
@keyframes fadeIn{from{opacity:0;}}
@keyframes viewIn{from{opacity:0;transform:translateY(8px);}}
@keyframes riseIn{from{opacity:0;transform:translateY(12px) scale(.985);}}
@keyframes msgIn{from{opacity:0;transform:translateY(10px) scale(.94);}}
@keyframes rowIn{from{opacity:0;transform:translateX(-8px);}}
@keyframes popIn{from{opacity:0;transform:scale(.9);}}

.view-in{animation:viewIn .34s var(--ease) both;}

/* ---- between one route and the next ---------------------------------- */
/* Navigating swapped the whole page in one frame, which the eye reads as a
   flicker rather than as a change. The browser cross-fades a snapshot of the
   old page into the new one; all this does is give that the same short,
   settling weight the rest of the motion here has. It is opacity and
   transform on two snapshots the compositor already holds, so it costs a
   composite and no layout.

   Vertical on purpose: this interface runs both ltr and rtl, and a sideways
   transition means the opposite thing in the two. Down is down in both.

   The blanket reduced-motion rule below cannot reach these — `*` does not
   match a pseudo-element — so they opt out on their own, and paintRoute()
   in app.js does not start a transition in the first place. */
@media (prefers-reduced-motion:no-preference){
  ::view-transition-old(root),
  ::view-transition-new(root){
    animation-duration:.26s;
    animation-timing-function:var(--ease);
  }
  ::view-transition-old(root){animation-name:routeOut;}
  ::view-transition-new(root){animation-name:routeIn;}
}
@keyframes routeOut{to{opacity:0;transform:translateY(-6px);}}
@keyframes routeIn{from{opacity:0;transform:translateY(10px);}}

/* Dashboard tiles and cards arrive in a short stagger. */
.grid > *{animation:riseIn .42s var(--spring) both;}
.grid > *:nth-child(2){animation-delay:.045s;}
.grid > *:nth-child(3){animation-delay:.09s;}
.grid > *:nth-child(4){animation-delay:.135s;}
.grid > *:nth-child(5){animation-delay:.18s;}
.grid > *:nth-child(6){animation-delay:.225s;}

.msg.is-new{animation:msgIn .38s var(--spring) both;}
.trow.is-new,.lrow.is-new{animation:rowIn .34s var(--ease) both;}
.pill{transition:background .2s var(--ease),color .2s var(--ease);}

/* Nav selection glides instead of snapping. */
.nav-a,.tab{transition:background .22s var(--ease),color .18s var(--ease);}
.tile.link{transition:transform .24s var(--spring),background .2s;}
.tile.link:hover{transform:translateY(-2px);}
.trow,.lrow{transition:background .16s var(--ease);}
/* `will-change:transform` used to sit here on .btn, .chip, .icon-btn and
   .cbtn — every button, chip and icon button on the page, for the whole life
   of the page. It reads like a performance hint and is the opposite of one:
   it promotes each of them to its own compositor layer and holds that layer
   in GPU memory whether or not anything is animating. A chat list or an
   admin screen has dozens, and on the low-end Android phones this is mostly
   read on that memory is exactly what there is least of — enough of it and
   scrolling gets worse, which is the thing the hint was meant to help.
   A .16s transform transition on tap needs no hint; the compositor handles
   it either way. Removed rather than replaced. */

/* ==========================================================================
   RESPONSIVE
   Ordered widest → narrowest so each layer refines the one above it.
   ========================================================================== */

/* Laptop → tablet only. The lower bound matters: without it these still match
   on a phone and, being later in the file, would beat the mobile layout. */
@media (min-width:901px) and (max-width:1180px){
  :root{--nav-w:216px;}
  .chat{grid-template-columns:300px minmax(0,1fr);}
  .pad{padding-inline:20px;}
}
@media (min-width:901px) and (max-width:1024px){
  .chat{grid-template-columns:268px minmax(0,1fr);}
  .grid.g3{grid-template-columns:repeat(auto-fit,minmax(190px,1fr));}
  .conv-acts .btn span{display:none;}
}

/* Phones: rows become stacks, and nothing overflows sideways. */
@media (max-width:720px){
  .pad{padding:16px 14px 96px;}
  .sec-h{flex-wrap:wrap;gap:8px;margin:24px 0 10px;}
  .sec-h h2{font-size:1.12rem;}
  .sec-h .btn,.sec-h .more{margin-inline-start:0;}
  .grid{gap:10px;}
  .grid.g3,.grid.g2{grid-template-columns:1fr 1fr;}
  .tile{padding:15px 16px;}
  .tile .v{font-size:1.6rem;}
  .two,.brand-row{grid-template-columns:1fr;}
  .auth-card{padding:22px 18px;border-radius:var(--r-l);}
  .conv-acts{gap:5px;}
  .conv-acts .btn{padding:0 9px;font-size:.78rem;}
  .lrow,.trow{padding:11px 12px;}
  .lrow .r{gap:6px;}
  .tbl th,.tbl td{padding:10px 12px;white-space:nowrap;}
  .bub{font-size:.97rem;}
  .bub.media img,.bub.media.vid video{max-width:74vw;}
  .msgs{padding:14px 12px 6px;}
  .composer{padding:8px 10px 12px;}
  body[data-credit="on"] .credit{font-size:.6rem;}
  .drop{padding:18px 14px;}
  .head{padding-inline:14px;}
  .head h1{font-size:.98rem;}
  /* A sheet on a phone should be a bottom sheet. */
  #sheet{padding:0;align-items:flex-end;}
  #sheet .card{width:100%;max-height:88vh;max-height:88dvh;border-radius:var(--r-xl) var(--r-xl) 0 0;}
  #sheet .sheet-b{padding-bottom:env(safe-area-inset-bottom);}
}

@media (max-width:400px){
  .grid.g3,.grid.g2{grid-template-columns:1fr;}
  .tab{font-size:.58rem;}
  .btn.lg{height:44px;font-size:.96rem;}
  .av.l{width:48px;height:48px;font-size:.95rem;}
}

/* Landscape phones have almost no height to spare. */
@media (max-height:520px) and (orientation:landscape){
  :root{--head-h:46px;}
  .hero{min-height:auto;padding-block:36px 44px;}
}

/* Touch devices get finger-sized targets. */
@media (pointer:coarse){
  .btn.sm{height:34px;padding:0 13px;}
  .icon-btn,.cbtn{width:40px;height:40px;}
  .chip{padding:8px 13px;}
  .seg button{padding:9px 15px;}
  .lrow,.trow{min-height:52px;}
}

/* ==========================================================================
   PHONE POLISH
   The small screen gets its own treatment, not a squeezed desktop: a floating
   glass dock, large tight titles, and roomier grouped cards.
   ========================================================================== */
@media (max-width:900px){

  /* --- floating dock ---------------------------------------------------- */
  .tabbar{
    inset-inline:12px;
    inset-block-end:calc(10px + env(safe-area-inset-bottom));
    padding:6px;border-radius:24px;border:1px solid var(--glass-line);
    box-shadow:0 10px 30px -8px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.10);
    -webkit-backdrop-filter:saturate(200%) blur(26px);
    backdrop-filter:saturate(200%) blur(26px);
    gap:2px;
  }
  :root[data-theme="dark"] .tabbar,:root[data-theme="dark"] .tabbar{
    box-shadow:0 12px 34px -8px rgba(0,0,0,.7), 0 0 0 .5px rgba(255,255,255,.06);
  }
  .tab{
    border-radius:18px;padding:8px 4px 7px;gap:4px;
    transition:background .28s var(--ease),color .2s var(--ease);
  }
  .tab.on{background:var(--accent-soft);}
  .tab svg{transition:transform .34s var(--spring);}
  .tab.on svg{transform:translateY(-1px) scale(1.12);}
  .tab.on span{font-weight:600;}
  .tab:active{transform:scale(.94);}

  /* clear the dock */
  .pad{padding-bottom:104px;}
  /* Resting right on top of the floating dock, in its corner. */
  body[data-credit="on"]:has(.tabbar:not(.hide)) .credit{inset-block-end:calc(82px + env(safe-area-inset-bottom));}

  /* --- large, tight titles --------------------------------------------- */
  .head{height:auto;min-height:var(--head-h);padding:11px 16px;}
  .head h1{font-size:1.3rem;letter-spacing:-.032em;}
  .head .sub{font-size:.78rem;margin-top:1px;}

  /* --- roomier grouped cards ------------------------------------------- */
  .card{border-radius:20px;}
  .tile{border-radius:20px;}
  .grid > *{animation-duration:.36s;}
  .sec-h{padding-inline:2px;}

  /* the first stat tile carries the brand tint so the screen has a focus */
  .grid.g3 > :first-child{
    background:linear-gradient(150deg,var(--accent-soft),var(--surface) 78%);
  }

  /* --- conversation header stays on one line --------------------------- */
  .chat{grid-template-columns:minmax(0,1fr) !important;}
  .conv-head{padding:8px 12px;gap:9px;min-height:52px;flex-wrap:nowrap;}
  /* The actions are a direct div child of the header too, so this rule was
     hitting them as well as the name — and `.conv-head > div` (0,1,1) outranks
     `.conv-acts` (0,1,0), so it beat the flex:none below and gave the buttons
     flex:1 with min-width:0. That let them shrink under their own labels, and
     "Mark resolved" ended up past the right edge of a phone with nothing to
     tap. The name is the only thing here that should give way. */
  .conv-head > div:not(.conv-acts){min-width:0;flex:1;}
  .conv-head .who-t{
    font-size:.94rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }
  .conv-head .sub-t{
    font-size:.71rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }
  .conv-head .av.m{width:32px;height:32px;font-size:.7rem;}
  /* Never shrink and never be squeezed out: whatever room is left after the
     buttons is the name's, not the other way round. */
  .conv-acts{flex:0 0 auto;gap:6px;}
  .conv-acts .pill{display:none;}          /* status already shows in the list */
  .conv-acts .btn{
    height:30px;padding:0 10px;font-size:.78rem;
    /* A label that wrapped grew the button taller than the row and pushed its
       own text out of the tappable box. */
    white-space:nowrap;flex:0 0 auto;
  }
  #backBtn{margin-inline-start:-4px;}

  /* rating strip and composer keep their own rows */
  .typing{padding:0 14px 2px;height:16px;font-size:.74rem;}

  /* Long equations must not burst the bubble; they scroll inside it. */
  .bub{max-width:100%;}
  .katex-display{
    font-size:.9em;max-width:100%;overflow-x:auto;overflow-y:hidden;
    -webkit-overflow-scrolling:touch;padding-bottom:4px;
  }
  .katex-display > .katex{white-space:nowrap;}
  .bub .katex{font-size:.98em;}

  /* --- chat feels native ------------------------------------------------ */
  .bub{border-radius:22px;}
  .msg.out .bub{border-end-end-radius:8px;}
  .msg.in .bub{border-end-start-radius:8px;}
  .trow{grid-template-columns:44px minmax(0,1fr) auto;}
  .trow .av.m{width:44px;height:44px;font-size:.86rem;}
  .trow:not(:last-child)::after{inset-inline-start:70px;}

  /* A conversation takes the whole screen; the dock would cover the composer,
     so it steps aside — the back arrow brings it back. Driven by a body
     attribute rather than :has(), which older phone browsers lack. */
  body[data-pane="conv"] .tabbar{display:none;}
  body[data-pane="conv"] .credit{display:none !important;}
  .composer{padding-bottom:calc(12px + env(safe-area-inset-bottom));}
  /* Belt and braces: give the conversation a real height on every engine. */
  .chat > *{min-height:0;}
  .chat[data-pane="conv"] .conv{height:100%;}

  /* --- controls ---------------------------------------------------------- */
  .btn.lg{border-radius:16px;}
  .seg{border-radius:12px;}
  .seg button{border-radius:9px;}
  .search{border-radius:12px;padding:10px 13px;}
}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;}
  .msgs{scroll-behavior:auto;}
}

/* ============================================================== LIVE ROOM */
.live-room{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px;align-items:start;}

/* --------------------------------------------------------- the whiteboard */
.wb{background:var(--surface);border-radius:var(--r-xl);box-shadow:var(--sh-1);overflow:hidden;}
.wb-bar{
  display:flex;align-items:center;gap:11px;flex-wrap:wrap;
  padding:10px 12px;border-bottom:1px solid var(--line);background:var(--surface-2);
}
.wb-tools,.wb-colors,.wb-acts,.wb-insert{display:flex;align-items:center;gap:4px;}
.wb-insert{gap:6px;}
.wb-t{
  width:34px;height:34px;border-radius:9px;display:grid;place-items:center;
  color:var(--label-2);transition:background .16s var(--ease),color .16s var(--ease);
}
.wb-t:hover{background:var(--surface-3);color:var(--label-1);}
.wb-t.on{background:var(--accent);color:#fff;}
.wb-c{
  width:21px;height:21px;border-radius:50%;background:var(--c);color:var(--label-1);
  box-shadow:0 0 0 1px rgba(0,0,0,.14) inset;
  transition:transform .16s var(--spring),box-shadow .16s var(--ease);
}
.wb-c:hover{transform:scale(1.12);}
.wb-c.on{box-shadow:0 0 0 2px var(--surface-2),0 0 0 4px var(--c);}
.wb-size{display:flex;align-items:center;gap:8px;color:var(--label-3);}
.wb-size input{width:74px;accent-color:var(--accent);}
.wb-acts{margin-inline-start:auto;}
.wb-acts .icon-btn:disabled{opacity:.35;cursor:not-allowed;}
/* There is a chat column beside the board at this width, so nothing to open. */
.wb-chat{display:none;}
.wb-stage{
  position:relative;background:var(--surface);height:min(66vh,620px);outline:none;
}
.wb-stage canvas{display:block;touch-action:none;}
/* Where you are on an endless board, and the way back to the middle of it. */
.wb-zoom{
  position:absolute;inset-block-end:12px;inset-inline-end:12px;z-index:3;
  display:flex;align-items:center;gap:2px;padding:3px;
  background:var(--surface-2);border:1px solid var(--line);
  border-radius:999px;box-shadow:var(--sh-1);
}
.wb-zoom .icon-btn{width:28px;height:28px;}
.wb-zoom .zlabel{
  min-width:52px;padding:0 4px;font-size:.76rem;font-weight:600;
  color:var(--label-2);font-variant-numeric:tabular-nums;
}
.wb-zoom .zlabel:hover{color:var(--accent);}
.wb-text{
  position:absolute;transform:translateY(-2px);z-index:2;
  background:transparent;border:0;border-bottom:2px dashed var(--accent);
  font-family:var(--font-d);font-weight:600;min-width:120px;padding:0 2px;outline:none;
}
/* Rewriting a sticky note happens in a textarea laid over the card itself, in
   the card's own colour, so what you type is already where it will end up. */
.wb-note-edit{
  position:absolute;z-index:3;box-sizing:border-box;resize:none;
  border:0;border-radius:12px;outline:2px solid var(--accent);
  font-family:var(--font-d);color:#1f2937;box-shadow:var(--sh-2);
}
.wb-foot{
  padding:9px 14px;border-top:1px solid var(--line);background:var(--surface-2);
  font-size:.78rem;
}

/* -------------------------------------------------------- the meeting chat */
.live-chat{
  display:flex;flex-direction:column;background:var(--surface);
  border-radius:var(--r-xl);box-shadow:var(--sh-1);overflow:hidden;
  height:calc(min(66vh,620px) + 111px);
}
.lc-head{
  display:flex;align-items:center;gap:9px;padding:13px 15px;
  border-bottom:1px solid var(--line);background:var(--surface-2);
}
.lc-head svg{color:var(--accent);}
/* The count sits at the far end; the dismiss only exists where the panel is a
   drawer over the board, which is phones. */
.lc-head #lcviewers{margin-inline-start:auto;}
.lc-x{display:none;flex:none;width:30px;height:30px;}
.lc-head b{font-size:.95rem;font-weight:600;}
.live-chat .msgs{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:14px;}
.live-chat .composer{border-top:1px solid var(--line);}

@media (max-width:1100px){
  .live-room{grid-template-columns:1fr;}
  .live-chat{height:420px;}
}
@media (max-width:640px){
  .wb-size{display:none;}
  .wb-stage{height:56vh;}
  .wb-insert .btn span{display:none;}
}

/* In a chat it has a line of its own under the box you type in, so it can
   never sit on a message or on what is being typed. */
.chat-credit{flex:none;margin:0;padding:0 18px calc(8px + env(safe-area-inset-bottom));text-align:right;background:var(--glass);
  font-size:.68rem;font-weight:500;letter-spacing:.02em;color:var(--label-4);user-select:none;}
.conv:has(.chat-credit) .composer{padding-bottom:4px;}
/* One face for the name wherever it appears, in either language. */
.chat-credit,.live-credit,.credit{font-family:var(--font);}

/* Recording: the mark on the presenter's card, and the list of past workshops. */
.oa-rec{display:inline-flex;align-items:center;gap:5px;margin-inline-start:10px;padding:2px 8px;
  border-radius:99px;background:var(--crit);color:#fff;font-size:.68rem;font-weight:700;
  letter-spacing:.04em;vertical-align:middle;}
.oa-rec[hidden]{display:none;}
.oa-rec i{width:6px;height:6px;border-radius:50%;background:#fff;animation:recBlink 1.4s ease-in-out infinite;}
@keyframes recBlink{50%{opacity:.25;}}
@media (prefers-reduced-motion:reduce){ .oa-rec i{animation:none;} }
.rec-row{align-items:center;gap:12px;}
.rec-row .t,.rec-row .s{display:block;}
.rec-ic{flex:none;width:38px;height:38px;border-radius:10px;display:grid;place-items:center;
  background:var(--accent-soft);color:var(--accent);}
.rec-acts{display:flex;align-items:center;gap:6px;flex:none;}
.rec-video{display:block;width:100%;max-height:72vh;background:#000;}
#wsPast .sec-h{margin-top:22px;}

/* The studio signs off at the very foot of the page, once you have scrolled
   past the board. */
.live-credit{
  margin:18px 2px 0;text-align:right;
  font-size:.68rem;font-weight:500;letter-spacing:.02em;color:var(--label-4);
  user-select:none;
}

/* --------------------------------------------------------- the live hub
   One page where hosting and joining are the same decision, taken in front of
   the rooms that actually exist. */
.host-card{
  display:flex;align-items:center;gap:16px;padding:20px 22px;margin-bottom:22px;
  background:linear-gradient(140deg,var(--accent-soft),var(--surface) 72%);
}
.host-card .hc-i{
  flex:none;width:46px;height:46px;border-radius:14px;display:grid;place-items:center;
  background:var(--accent);color:var(--on-accent);
}
.host-card .hc-t{font-size:1.06rem;font-weight:600;letter-spacing:-.019em;}
.host-card .hc-s{font-size:.9rem;color:var(--label-2);margin-top:3px;max-width:52ch;}
@media (max-width:640px){
  .host-card{flex-wrap:wrap;}
  .host-card .btn{width:100%;justify-content:center;}
}

.lrooms{display:flex;flex-direction:column;gap:12px;}
.lroom{
  background:var(--surface);border-radius:var(--r-xl);box-shadow:var(--sh-1);
  padding:16px 18px;display:flex;flex-direction:column;gap:13px;
  animation:riseIn .34s var(--ease) both;
}
.lroom.own{box-shadow:0 0 0 1.5px var(--accent) inset,var(--sh-1);}
.lr-top{display:flex;align-items:center;gap:12px;}
/* Every room on this page is live by definition, so the dot says so rather
   than sitting grey the way it does on the presenter's idle card. */
.lroom .oa-dot{
  background:var(--crit);box-shadow:0 0 0 5px color-mix(in srgb,var(--crit) 18%,transparent);
  animation:oaPulse 1.9s ease-in-out infinite;
}
/* "3 watching" is a count, not a status word — capitalising it reads wrong. */
.lroom .pill{text-transform:none;}
@media (prefers-reduced-motion:reduce){ .lroom .oa-dot{animation:none;} }
.lr-t{font-size:1.02rem;font-weight:600;letter-spacing:-.018em;display:flex;align-items:center;gap:8px;}
.lr-s{font-size:.86rem;color:var(--label-3);margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.lr-who{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding-top:12px;border-top:1px solid var(--line);
}
.lr-wl{font-size:.78rem;font-weight:600;color:var(--label-3);}
.lr-avs{display:flex;}
.lr-avs .av.m{width:30px;height:30px;font-size:.68rem;border:2px solid var(--surface);}
.lr-avs .av.m:not(:first-child){margin-inline-start:-9px;}
.lr-avs .av.m.more{background:var(--surface-3);color:var(--label-2);font-weight:600;}
.lr-names{font-size:.85rem;color:var(--label-2);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.lr-names.dim{color:var(--label-3);}
.lr-go{align-self:flex-start;}
@media (max-width:520px){ .lr-go{width:100%;justify-content:center;} }

/* ------------------------------------------------------------- on air */
/* The switch that puts a JOIN LIVE! button on every question sent while the
   room is open. */
.on-air{
  display:flex;align-items:center;gap:14px;flex-wrap:wrap;
  padding:13px 16px;margin-bottom:14px;
  background:var(--surface);border-radius:var(--r-xl);box-shadow:var(--sh-1);
}
.on-air.live{box-shadow:0 0 0 1.5px var(--crit) inset,var(--sh-1);}
.oa-dot{width:10px;height:10px;border-radius:50%;background:var(--label-4);flex:none;}
.on-air.live .oa-dot{
  background:var(--crit);box-shadow:0 0 0 5px color-mix(in srgb,var(--crit) 18%,transparent);
  animation:oaPulse 1.9s ease-in-out infinite;
}
@keyframes oaPulse{50%{opacity:.42;}}
.oa-t{font-weight:600;font-size:.97rem;}
.oa-s{font-size:.82rem;color:var(--label-3);}
.oa-title{max-width:250px;}
/* Whether the room is actually hearing the microphone. A presenter cannot tell
   from their own side — the meter moves and the board looks right whether or
   not a single packet is leaving the machine — so this says it outright. */
.oa-reach{font-size:.78rem;font-weight:600;margin-top:3px;}
.oa-reach.ok{color:var(--ok);}
.oa-reach.warn{color:var(--crit);}
.oa-reach:empty{display:none;}

/* --------------------------------------------- the join-live card in a thread */
.live-card{
  align-self:center;max-width:min(520px,88%);margin:12px 0;padding:15px 18px;
  background:var(--surface-2);border:1px solid var(--line);
  border-radius:var(--r-l);text-align:center;
}
.live-card.on{border-color:var(--crit);background:var(--crit-soft);}
.lk-top{
  display:flex;align-items:center;justify-content:center;gap:7px;
  font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--label-3);
}
.live-card.on .lk-top{color:var(--crit);}
.live-card.on .lk-top svg{animation:oaPulse 1.9s ease-in-out infinite;}
.live-card p{margin:9px 0 0;font-size:.93rem;color:var(--label-2);}
.lk-join{margin-top:13px;font-weight:700;letter-spacing:.05em;}
.lk-time{display:block;margin-top:9px;font-size:.7rem;color:var(--label-4);}

/* ------------------------------------------------------------ fullscreen */
/* The board and the chat go up together: during a session the questions coming
   in are half of what you are looking at. */
.live-room:fullscreen{
  height:100vh;padding:14px;gap:14px;background:var(--bg);
  grid-template-columns:minmax(0,1fr) 340px;align-items:stretch;
}
.live-room:fullscreen .wb{display:flex;flex-direction:column;min-height:0;}
.live-room:fullscreen .wb-stage{flex:1;height:auto;min-height:0;}
.live-room:fullscreen .live-chat{height:100%;}
@media (max-width:900px){
  .live-room:fullscreen{grid-template-columns:1fr;grid-template-rows:1fr 40vh;}
  .live-room:fullscreen .live-chat{height:100%;}
}

/* The microphone that will go out with the board, and what it is hearing. */
.oa-mic{position:relative;overflow:hidden;}
.oa-mic.on{background:var(--crit);color:#fff;border-color:transparent;}
.oa-mic.muted{background:var(--surface-3);color:var(--label-3);}
.oa-level{
  position:absolute;inset-inline:0;inset-block-end:0;height:3px;
  background:currentColor;opacity:.85;
}
.oa-level i{
  display:block;height:100%;background:currentColor;
  transform:scaleX(0);transform-origin:left center;transition:transform .09s linear;
}
.oa-mic:not(.on) .oa-level{display:none;}

/* Still being built, said where the work is happening. */
.soon-tag{
  display:inline-block;margin-inline-start:8px;padding:2px 8px;border-radius:999px;
  background:var(--accent-soft);color:var(--accent);vertical-align:2px;
  font-size:.62rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
}

/* The student's side of the board: watched, not drawn on. */
.wb-stage.viewer{cursor:default;--paper:#FFFFFF;}
.wb-stage.viewer canvas{touch-action:auto;}
.v-wait{
  position:absolute;inset:0;display:grid;place-items:center;
  pointer-events:none;padding:20px;
}
.vw-card{
  display:grid;justify-items:center;gap:5px;text-align:center;max-width:34ch;
  padding:22px 26px;border-radius:var(--r-xl);
  background:var(--surface-2);border:1px solid var(--line);box-shadow:var(--sh-1);
}
.vw-card b{font-size:.98rem;font-weight:600;letter-spacing:-.01em;color:var(--label-1);}
.vw-card span:last-child{font-size:.82rem;color:var(--label-3);line-height:1.5;}
/* A steady dot for a room that is simply quiet; it only pulses while there is
   actually something outstanding, so a calm state never looks like a busy one. */
.vw-dot{
  width:9px;height:9px;border-radius:50%;background:var(--ok);margin-bottom:5px;
}
.v-wait.busy .vw-dot{background:var(--accent);animation:pulse 1.1s infinite;}

/* A session in progress, said on the student's own home page. */
.live-banner{
  display:flex;align-items:center;gap:13px;padding:14px 16px;margin-bottom:16px;
  background:var(--surface);border-radius:var(--r-xl);
  box-shadow:0 0 0 1.5px var(--crit) inset,var(--sh-1);
  color:inherit;text-decoration:none;
}
.live-banner:hover{transform:translateY(-1px);}
.live-banner .oa-dot{
  background:var(--crit);box-shadow:0 0 0 5px color-mix(in srgb,var(--crit) 18%,transparent);
  animation:oaPulse 1.9s ease-in-out infinite;
}
.lb-t{display:block;font-weight:600;font-size:.98rem;}
.lb-s{display:block;font-size:.83rem;color:var(--label-3);}
.live-banner .btn{flex:none;}

/* The live tab wears a dot while the room is open. */
.nav-a.is-live,.tab.is-live{position:relative;}
.nav-a.is-live svg,.tab.is-live svg{color:var(--crit);}
.nav-a.is-live::after,.tab.is-live::after{
  content:"";position:absolute;width:7px;height:7px;border-radius:50%;
  background:var(--crit);animation:oaPulse 1.9s ease-in-out infinite;
}
.nav-a.is-live::after{inset-inline-end:14px;top:50%;transform:translateY(-50%);}
.tab.is-live::after{top:6px;inset-inline-end:calc(50% - 16px);}

/* Which year a session is for, and the other years running beside it. */
.oa-year{flex:none;}
.oa-others{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px;}
.oa-others .tag{font-size:.72rem;}
/* A colleague's session, and the way into it. It reads as a chip rather than a
   button because it is information first — that room exists — and a door second. */
.oa-others .tag.join{
  display:inline-flex;align-items:center;gap:6px;
  color:var(--accent);font-weight:600;transition:background .16s,color .16s;
}
.oa-others .tag.join svg{width:13px;height:13px;flex:none;}
.oa-others .tag.join:hover{background:var(--accent);color:var(--on-accent);}
.pill.tap{cursor:pointer;}
.pill.tap:hover{filter:brightness(1.08);}
/* Claiming or resolving changes this pill and nothing else on screen moves, so
   the one thing that did change says so. */
.pill.pop{animation:pillPop .46s var(--spring);}
@keyframes pillPop{0%{transform:scale(.84);}58%{transform:scale(1.13);}100%{transform:scale(1);}}
@media (prefers-reduced-motion:reduce){ .pill.pop{animation:none;} }
/* Somebody is asking to come back in. The pill sits at the top of the chat
   during a lesson, so it has to be noticed without being shouted at. */
.pill.knock{
  background:var(--crit-soft);color:var(--crit);
  animation:oaPulse 1.9s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){ .pill.knock{animation:none;} }

/* ------------------------------------------------------------- the lobby */
/* The way into a live session, used twice: as a menu over the page a student
   is already reading, and as the landing screen when the room is opened on
   its own. It is the same card both times, and it is the same card as every
   other card in the app — a session is an occasion, not a different product. */
.lobby-pad{display:grid;place-items:center;min-height:min(72dvh,620px);}
.lobby{
  position:relative;width:min(520px,100%);overflow:hidden;text-align:center;
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-xl);box-shadow:var(--sh-3);
}
/* A wash of the accent behind the badge, so the card reads as a way in rather
   than as one more panel. Nothing louder than that. */
.lb-wash{
  position:absolute;inset-block-start:-120px;inset-inline:-40px;height:220px;z-index:0;
  background:radial-gradient(58% 100% at 50% 100%,var(--accent),transparent 70%);
  opacity:.13;pointer-events:none;
}
.lb-inner{position:relative;z-index:1;padding:34px 26px 24px;}

.lb-badge{
  display:inline-flex;align-items:center;gap:8px;padding:5px 13px;border-radius:999px;
  background:var(--crit-soft);color:var(--crit);
  font-size:.66rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
}
/* The same breathing dot the banner wears, so "live" looks like "live"
   wherever it is said. */
.lb-badge .oa-dot{
  background:var(--crit);box-shadow:0 0 0 4px color-mix(in srgb,var(--crit) 16%,transparent);
  animation:oaPulse 1.9s ease-in-out infinite;
}
.lb-title{
  margin:16px 0 6px;color:var(--label-1);font-size:clamp(1.3rem,4.2vw,1.75rem);
  line-height:1.2;letter-spacing:-.02em;
}
.lb-by{margin:0;color:var(--label-2);font-size:.93rem;}
.lb-by span{opacity:.45;margin:0 6px;}

.lb-acts{display:flex;flex-direction:column;gap:9px;margin:24px auto 0;max-width:320px;}
/* Taken once it has made a noise, and says so. */
.lb-check.ok{background:var(--ok-soft);color:var(--ok);}
.lb-hint{margin:14px auto 0;max-width:36ch;font-size:.82rem;line-height:1.45;}
.lb-back{
  display:inline-block;margin-block-start:16px;color:var(--label-3);
  font-size:.86rem;text-decoration:none;
}
.lb-back:hover{color:var(--label-1);text-decoration:underline;}

/* As a menu, over the page it was opened from. */
#joinmenu{padding:20px;}
#joinmenu .lobby{width:min(440px,100%);animation:sheetIn .4s var(--spring);}
.jm-x{
  position:absolute;inset-block-start:12px;inset-inline-end:12px;z-index:2;
  color:var(--label-3);
}
.jm-x:hover{color:var(--label-1);}

@media (max-width:640px){
  .lobby-pad{min-height:auto;}
  .lb-inner{padding:28px 20px 20px;}
}
@media (prefers-reduced-motion:reduce){
  .lb-badge .oa-dot{animation:none;}
  #joinmenu .lobby{animation:none;}
}

/* ------------------------------------------------- watching, full screen */
/* Native fullscreen where a browser has it, and a viewport-filling stand-in
   where it does not (iPhones), so the two look the same to a student. */
.live-room.faux-full{
  position:fixed;inset:0;z-index:60;margin:0;padding:12px;gap:12px;
  background:var(--bg);height:100dvh;
}
.live-room:fullscreen{padding:12px;gap:12px;background:var(--bg);}
body.viewing-full{overflow:hidden;}
body.viewing-full .tabbar{display:none;}

/* Fullscreen on a phone cannot be all board: the presenter's window is a
   different shape from a phone screen, so a band is left above and below it.
   That band belongs to the room rather than to the board — black, the way any
   other video watched full screen is black, so nothing competes with the
   working. The paper itself is painted into the canvas at the presenter’s
   exact shape, which is what keeps the board white inside all of this. */
body.viewing-full .live-room{background:#000;}
body.viewing-full .wb{background:#000;box-shadow:none;}
body.viewing-full .wb-stage.viewer{background:#000;}
body.viewing-full .wb-foot{display:none;}

/* A student's board takes the whole width until they ask for the chat: an
   empty column beside it is whiteboard they paid for. */
.live-room:has(#vwb){grid-template-columns:minmax(0,1fr);}
.live-room:has(#vwb) .live-chat{display:none;}
.live-room:has(#vwb).chat-open .live-chat{display:flex;}

/* On a laptop the chat is a column on the right — including in fullscreen,
   where the board has the most to lose from a panel across the bottom.
   The panel is stretched to the board's height rather than sized by what is
   in it: height:100% alone was cyclic against a start-aligned auto row, so it
   resolved to auto and the column grew with every message until the room
   scrolled the page instead of the chat. height:0 keeps the messages out of
   the row calculation — the board sizes the row — and min-height:100% then
   fills it, which is also what fullscreen wants. */
@media (min-width:901px){
  .live-room:has(#vwb).chat-open{
    grid-template-columns:minmax(0,1fr) 340px;align-items:stretch;
  }
  .live-room:has(#vwb).chat-open .live-chat{
    position:static;height:0;min-height:100%;
  }
}

/* On a phone it is a drawer over the board instead, because a column that
   narrow is neither a chat nor a whiteboard. Both rooms: the presenter's was
   left out of all of this, so a phone gave the host a 420px slab of chat under
   the board, running off the bottom of the screen with nothing to dismiss it —
   and the board it was pushing away is the whole point of that page. Every
   selector below names both boards rather than one, because `:has(#vwb)`
   carries an id's specificity and a plain `.live-room` rule cannot outrank it. */
@media (max-width:900px){
  .live-room:has(#wb) .live-chat{display:none;}
  .live-room:has(#wb).chat-open .live-chat{display:flex;}

  .live-room:has(#vwb).chat-open .live-chat,
  .live-room:has(#wb).chat-open .live-chat{
    position:fixed;inset-block:auto 0;inset-inline:0;z-index:70;
    height:var(--chat-drawer-h);border-radius:var(--r-xl) var(--r-xl) 0 0;
    box-shadow:0 -14px 40px rgba(0,0,0,.28);
  }
  /* Inside a room that is itself the fullscreen element the drawer belongs to
     that box, not the viewport. These carry the same id in the :has() so they
     can actually outrank the rule above — without it they never applied. */
  .live-room:has(#vwb).faux-full.chat-open .live-chat,
  .live-room:has(#vwb):fullscreen.chat-open .live-chat,
  .live-room:has(#wb).faux-full.chat-open .live-chat,
  .live-room:has(#wb):fullscreen.chat-open .live-chat{position:absolute;}

  /* The drawer came up over the board's control pill — which carries the sound
     button, fullscreen, and the very button that opens and closes this drawer.
     Opening the chat therefore took away the only way to close it, and the
     lesson's volume with it. The controls ride above the drawer instead. */
  .live-room:has(#vwb).chat-open .v-ctl{
    inset-block-end:calc(var(--chat-drawer-h) + 8px);
  }
  /* The composer sat flush on the bottom edge, under an iPhone's home bar. */
  .live-room:has(#vwb).chat-open .live-chat .composer,
  .live-room:has(#wb).chat-open .live-chat .composer{
    padding-bottom:calc(10px + env(safe-area-inset-bottom));
  }
  /* The presenter's way in and out of that drawer. It leads the toolbar strip
     so it is reachable without swiping the strip first. */
  .wb-chat{display:grid;order:-1;}
  .wb-chat.on{background:var(--accent);color:#fff;}
  /* The dock was not hidden, only buried under the drawer — the same thing a
     conversation does when it takes the screen. */
  body.chat-drawer .tabbar{display:none;}
  body.chat-drawer .credit{display:none !important;}
  /* A drawer needs a way out that does not depend on the board behind it. */
  .lc-x{display:grid;}
}

.live-room.faux-full .wb,
.live-room:fullscreen .wb{display:flex;flex-direction:column;min-height:0;height:100%;}
.live-room.faux-full .wb-stage,
.live-room:fullscreen .wb-stage{flex:1;height:auto;min-height:0;}
.live-room.faux-full .live-chat,
.live-room:fullscreen .live-chat{height:100%;}

/* The unread count on the chat button. */
.vc-n{font-size:.72rem;font-weight:700;}
.vc-n.on{
  min-width:18px;height:18px;border-radius:999px;background:var(--crit);color:#fff;
  display:inline-grid;place-items:center;margin-inline-start:6px;
}

/* The watcher's controls ride on the board, so fullscreen keeps them. */
.v-ctl{
  position:absolute;top:10px;inset-inline-end:10px;z-index:6;
  display:flex;align-items:center;gap:5px;padding:4px;
  background:var(--surface-2);border:1px solid var(--line);
  border-radius:999px;box-shadow:var(--sh-1);
}
.v-ctl .icon-btn{width:30px;height:30px;}
.v-ctl .btn.sm{padding:5px 11px;}

/* Full screen keeps the controls, but a notch and a home bar do not care
   that the page has gone edge to edge. */
body.viewing-full .v-ctl{
  top:calc(10px + env(safe-area-inset-top));
  inset-inline-end:calc(10px + env(safe-area-inset-right));
}
/* Out of the way while the lesson is being watched, back at a touch. The
   pill covers a corner of the board, and on a phone that corner is a third
   of the width. Kept clickable-adjacent: a tap anywhere brings it back
   before it can be missed. */
body.viewing-full .live-room.idle .v-ctl{
  opacity:0;transform:translateY(-8px);pointer-events:none;
}
.v-ctl{transition:opacity .35s var(--ease),transform .35s var(--ease);}

/* The board is the shape of the presenter's window, so a phone held upright
   shows it as a strip across the middle. Android is asked to turn; iPhones
   refuse, so they are asked instead. Shown only where it can be acted on: a
   phone, upright, with the board already filling the screen. */
.rotate-hint{display:none;}
@media (max-width:900px) and (orientation:portrait){
  body.viewing-full .rotate-hint{
    position:absolute;inset-inline:16px;inset-block-end:calc(16px + env(safe-area-inset-bottom));
    z-index:5;margin:0;padding:9px 14px;border-radius:999px;
    display:flex;align-items:center;justify-content:center;gap:8px;text-align:center;
    background:rgba(0,0,0,.62);color:#fff;font-size:.8rem;font-weight:500;
    backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
    animation:rotateHintIn .4s var(--ease) both;
  }
  body.viewing-full .live-room.idle .rotate-hint{opacity:0;transition:opacity .35s var(--ease);}
}
@keyframes rotateHintIn{from{opacity:0;transform:translateY(8px);}}
@media (prefers-reduced-motion:reduce){
  .v-ctl,.rotate-hint{transition:none;animation:none;}
}



/* A phone gets more board and less chrome. The placement of .v-ctl used to be
   set here too, at the top — but the max-width:720px block below matches every
   phone this one does and comes later, so that rule never once applied. The
   bottom placement down there is the one that was really in effect. */
@media (max-width:640px){
  .v-ctl .btn.sm span{display:none;}
}

/* The watched board is shaped in script to match the host's screen, so its
   height is set there rather than here. */
.wb-stage.viewer{height:auto;}
.live-room.faux-full .wb-stage.viewer,
.live-room:fullscreen .wb-stage.viewer{flex:0 0 auto;margin:auto 0;}

/* ==========================================================================
   THE LIVE ROOM ON A PHONE
   The board, the bar above it and the queue of new students were all built at
   laptop width. On a phone they need different shapes, not smaller ones.
   ========================================================================== */
@media (max-width:720px){

  /* The toolbar becomes one strip you swipe along, instead of four rows that
     eat the board it is supposed to serve. Nearly two thirds of it is off the
     end at this width — the colours, the insert buttons and every action — and
     with the scrollbar hidden there was nothing at all to say so. The mask
     fades the strip out at whichever end still has something on it, which is
     the one honest way a scroller can admit it. */
  .wb-bar{
    flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;
    gap:9px;padding:8px 10px;scrollbar-width:none;
    /* Two opaque covers pinned to the content (`local`) slide away as it
       scrolls, uncovering a shadow pinned to the strip itself — so an edge is
       shaded only while there is really something past it, and the end of the
       strip looks like the end. */
    background:
      linear-gradient(to right,var(--surface-2),var(--surface-2)) 0 0 / 22px 100% no-repeat local,
      linear-gradient(to left,var(--surface-2),var(--surface-2)) 100% 0 / 22px 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%,rgba(0,0,0,.13),rgba(0,0,0,0)) 0 0 / 16px 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%,rgba(0,0,0,.13),rgba(0,0,0,0)) 100% 0 / 16px 100% no-repeat scroll,
      var(--surface-2);
  }
  .wb-bar::-webkit-scrollbar{display:none;}
  .wb-tools,.wb-colors,.wb-insert,.wb-acts{flex:0 0 auto;}
  .wb-acts{margin-inline-start:0;}
  .wb-t{width:38px;height:38px;}
  .wb-c{width:25px;height:25px;}
  .wb-size{display:none;}                    /* the slider is a laptop control */
  .wb-insert .btn span{display:none;}
  .wb-foot{display:none;}                    /* keyboard hints mean nothing here */
  .wb-stage{height:min(58dvh,520px);}
  .wb-zoom{inset-block-end:8px;inset-inline-end:8px;}
  .wb-zoom .zlabel{min-width:44px;font-size:.72rem;}

  /* Going live: the year, the title and the two buttons each get a full line
     rather than being squeezed onto one. */
  .on-air{flex-wrap:wrap;gap:10px;padding:12px 14px;}
  .on-air .oa-t{font-size:.95rem;}
  .on-air .oa-s{font-size:.78rem;}
  .oa-year{order:4;flex:1 1 100%;}
  .oa-year button{flex:1;}
  .oa-title{order:5;flex:1 1 100%;max-width:none;}
  .oa-mic{order:6;flex:1 1 46%;justify-content:center;}
  #oatoggle{order:7;flex:1 1 46%;justify-content:center;}
  .oa-others{gap:5px;}

  /* A row that carries buttons puts them on their own line instead of
     squeezing the name that explains what they do. */
  .lrow:has(.btn){flex-wrap:wrap;}
  .lrow:has(.btn) .r{width:100%;justify-content:flex-end;margin-top:8px;}

  /* The watched board keeps its controls reachable with one thumb. */
  .v-ctl{top:auto;inset-block-end:8px;inset-inline-end:8px;}
}

/* iOS zooms the whole page when a field with small text takes focus, which
   leaves the layout stranded mid-screen. Sixteen pixels is the threshold. */
@media (pointer:coarse){
  .input,.select,.textarea,input,select,textarea{font-size:16px;}
  .wb-t,.icon-btn,.cbtn,.tab{touch-action:manipulation;}
}

/* A phone turned sideways for the board should spend its height on the board. */
@media (max-height:560px) and (orientation:landscape){
  .wb-stage{height:calc(100dvh - 132px);}
  .on-air{padding:8px 12px;}
  .on-air .oa-s{display:none;}
}

/* ------------------------------------------------------- the meeting tab
   /meet and /meet/host are the same two live views with the app taken away
   from around them: no sidebar, no header, and the board given the width
   those were using. Opened in a tab of its own, so closing the tab is the
   way out and nothing here needs a back button. */
body[data-meet="on"] .shell{grid-template-columns:minmax(0,1fr);}
body[data-meet="on"] .side,
body[data-meet="on"] .head{display:none;}
body[data-meet="on"] .body{overflow-y:auto;}
body[data-meet="on"] .pad{padding:12px;max-width:none;}

/* The board is sized outright rather than stretched by a chain of flex parents.
   A flex:1 chain collapses to nothing the moment one ancestor has no definite
   height, and a whiteboard of zero height is not a subtle bug — it is a blank
   screen with no way to tell what went wrong. */
body[data-meet="on"] .wb-stage{height:calc(100dvh - 200px);min-height:320px;}

/* A phone has no room for the chat beside the board, so it goes underneath
   rather than squeezing both into nothing. */
@media (max-width:820px){
  body[data-meet="on"] .live-room{grid-template-columns:minmax(0,1fr);}
  body[data-meet="on"] .wb-stage{height:58dvh;min-height:260px;}
}

/* Delete, on a message you are allowed to remove. Faint until the message is
   hovered or focused so it never competes with what was actually said; always
   visible on touch, where there is no hover to reveal it with. */
.mdel{
  border:0;background:none;cursor:pointer;color:var(--label-4);
  font-size:.78rem;line-height:1;padding:2px 4px;margin-inline-start:6px;
  border-radius:5px;opacity:0;transition:opacity .12s,color .12s,background .12s;
}
.msg:hover .mdel,.mdel:focus-visible{opacity:1;}
.mdel:hover{color:var(--danger,#e5484d);background:var(--surface-3);}
@media (hover:none){ .mdel{opacity:.55;} }

/* The room, when the presenter's connection has gone. It sits over the board
   rather than replacing it: the board is still correct, and the instinct on
   seeing a frozen lesson is to reload — which is the one action that costs you
   your place in it. */
.host-away{
  position:absolute;inset-block-start:12px;inset-inline:12px;z-index:4;
  display:grid;gap:2px;padding:12px 14px;border-radius:12px;
  background:var(--crit-soft);border:1px solid var(--crit);
  box-shadow:var(--sh-1);pointer-events:none;
}
.host-away b{font-size:.92rem;color:var(--crit);}
.host-away span{font-size:.8rem;color:var(--label-2);}

/* This page's own connection. Deliberately plain and out of the way. */
.offline-bar{
  position:fixed;inset-block-end:14px;left:50%;transform:translateX(-50%);
  z-index:9998;padding:8px 14px;border-radius:999px;
  background:var(--surface-3);color:var(--label-2);border:1px solid var(--line);
  font-size:.8rem;box-shadow:var(--sh-2);
}

/* The same pill, carrying an action. A new build is good news rather than a
   fault, so it reads as an offer and not as the offline warning above. */
.update-bar{display:flex;align-items:center;gap:10px;padding-inline-end:6px;color:var(--label)}
.update-bar .ub-go{
  border:0;border-radius:999px;padding:5px 12px;cursor:pointer;
  background:var(--accent);color:#fff;font:inherit;font-weight:600;font-size:.78rem;
}
.update-bar .ub-go:hover{filter:brightness(1.06)}
.update-bar .ub-go:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* =========================================================== POLISH PASS
   Everything below extends the vocabulary already in this file — the same
   --spring and --ease, the same short durations. The reduced-motion block
   above matches `*`, so every animation here is switched off for anyone who
   has asked their system for that. */

/* ------------------------------------------------------------ site notice
   The owner's message to the school, above everything else. */
.notice-bar{
  position:fixed;inset-inline:0;top:0;z-index:60;
  display:flex;align-items:center;gap:11px;padding:10px 16px;
  background:var(--accent);color:#fff;font-size:.9rem;font-weight:500;
  box-shadow:var(--sh-2);
  transform:translateY(-100%);transition:transform .28s var(--spring);
}
.notice-bar.in{transform:translateY(0);}
.notice-bar .nb-i{display:grid;place-items:center;opacity:.8;flex:none;}
.notice-bar .nb-t{flex:1;min-width:0;}
.notice-bar .nb-x{
  flex:none;color:#fff;opacity:.72;padding:5px;border-radius:7px;
  transition:opacity .16s,background .16s;
}
.notice-bar .nb-x:hover{opacity:1;background:rgba(255,255,255,.16);}
/* Reserve what the bar actually occupies, not one line's worth of it. The
   40px fallback is only for the frame before the measurement lands. */
body:has(.notice-bar.in) .shell{padding-top:var(--notice-h,40px);}
/* The drawer is fixed to the top of the window too, so on a phone the bar was
   sitting over the brand block at the head of it. */
@media (max-width:900px){
  body:has(.notice-bar.in) .side{padding-top:calc(12px + var(--notice-h,40px));}
}

/* -------------------------------------------------------------- skeletons
   A spinner says "something is happening"; a skeleton says "a list is
   arriving, and it will be about this shape" — which is the truer statement
   and stops the layout jumping when it lands. */
/* The shimmer was a moving background-position, which repaints the element
   on every frame it runs. Skeletons are on screen precisely when the device
   is already busy fetching and the connection is slow, so that is the worst
   moment to be asking for a repaint sixty times a second. Same look, done as
   a band translated across a clipped box: transform only, so it stays on the
   compositor and costs nothing to repaint, because it never repaints. */
@keyframes shimmer{to{transform:translateX(100%);}}
.skel{
  position:relative;overflow:hidden;
  border-radius:8px;
  background:var(--surface-3);
}
.skel::after{
  content:"";position:absolute;inset:0;
  transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--surface-2),transparent);
  animation:shimmer 1.4s linear infinite;
}
.skel-row{display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:12px;
  align-items:center;padding:14px 16px;border-bottom:1px solid var(--line);}
.skel-av{width:42px;height:42px;border-radius:50%;}
.skel-lines{display:flex;flex-direction:column;gap:7px;min-width:0;}
.skel-l1{height:11px;width:46%;}
.skel-l2{height:10px;width:76%;}
.skel-meta{height:10px;width:34px;}
.skel-tile{height:96px;border-radius:var(--r-l);}
/* Each row fades in a beat after the one above, so the block reads as a list
   rather than as one grey slab. */
.skel-row:nth-child(2){opacity:.85;}
.skel-row:nth-child(3){opacity:.7;}
.skel-row:nth-child(4){opacity:.55;}
.skel-row:nth-child(5){opacity:.4;}

/* ------------------------------------------------------- typing indicator */
.typing-dots{display:inline-flex;align-items:center;gap:3px;margin-inline-start:5px;
  vertical-align:middle;}
.typing-dots i{width:4px;height:4px;border-radius:50%;background:currentColor;
  animation:typingDot 1.25s ease-in-out infinite;}
.typing-dots i:nth-child(2){animation-delay:.16s;}
.typing-dots i:nth-child(3){animation-delay:.32s;}
@keyframes typingDot{0%,60%,100%{opacity:.28;transform:translateY(0);}
  30%{opacity:1;transform:translateY(-3px);}}

/* ------------------------------------------------------------ the toaster
   A bar that drains, so the toast says how long it intends to stay, and a
   drag that throws it away early. */
.toast{position:relative;overflow:hidden;touch-action:pan-y;cursor:grab;}
.toast::after{
  content:'';position:absolute;inset-inline:0;bottom:0;height:2px;
  background:currentColor;opacity:.28;transform-origin:inline-start;
  animation:toastDrain 3.2s linear forwards;
}
@keyframes toastDrain{from{transform:scaleX(1);}to{transform:scaleX(0);}}
.toast.go{opacity:0 !important;transform:translateY(-10px) scale(.96) !important;}

/* --------------------------------------------------------- the star rating
   The stars fill up to the one under the cursor rather than each lighting on
   its own, and the chosen one takes a beat to land. */
/* A closed conversation is rated before it is used again, so the rating sits
   where the composer would be rather than above it. Being the only thing there
   is what makes it required — no message asking politely, and nothing to
   scroll past. */
.rate-gate{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:14px 16px calc(14px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line); background:var(--surface-2);
  animation:replyBarIn .26s var(--ease) both;
}
.rate-gate .rg-t{ font-size:.94rem; font-weight:600; }
.rate-gate .rg-s{ font-size:.8rem; color:var(--label-3); flex:1 1 100%; order:3; }
.rate-gate .rg-stars{ display:flex; gap:4px; margin-inline-start:auto; }
.rate-gate .rg-stars button{ font-size:1.5rem; line-height:1; color:var(--label-4); }
@media (max-width:520px){
  .rate-gate .rg-stars{ margin-inline-start:0; }
  .rate-gate .rg-t{ flex:1 1 100%; }
}

#stars button{transition:color .14s var(--ease),transform .2s var(--spring);}
#stars button:hover{transform:scale(1.18);}
#stars button.lit{color:var(--warn);}
@keyframes starPop{40%{transform:scale(1.42) rotate(-8deg);}}
#stars button.picked{animation:starPop .42s var(--spring);}

/* ----------------------------------------------------------------- on air
   A ring that breathes out of the dot, so a room that is live reads as live
   from the corner of your eye. */
.on-air.live .oa-dot{position:relative;}
.on-air.live .oa-dot::after{
  content:'';position:absolute;inset:-4px;border-radius:50%;
  border:1.5px solid currentColor;animation:oaRing 2.1s ease-out infinite;
}
@keyframes oaRing{from{opacity:.55;transform:scale(.7);}to{opacity:0;transform:scale(1.9);}}

/* ------------------------------------------------------------ the numbers
   Counted up rather than stamped down, and never reflowing while they climb. */
.tile .v{font-variant-numeric:tabular-nums;}
.bar i{transition:width .7s var(--ease);}

/* ------------------------------------------------------- a message leaving
   Your own message springs from the composer's side rather than appearing. */
@keyframes msgOut{from{opacity:0;transform:translateY(14px) scale(.9);}}
.msg.out.is-new{animation:msgOut .34s var(--spring) both;}
.msg.sending .bub{opacity:.62;}
.msg.failed .bub{outline:1.5px solid var(--crit);outline-offset:2px;}

/* ==================================================== REQUEST A FEATURE
   A line of explanation, a form, and the requests under it — a student's own,
   or on the developer's screen everybody's, open ones first so the pile that
   still needs an answer is the pile you land on. */
.ideas-pad{max-width:760px;}
.ideas-lede{margin:0 0 16px;color:var(--label-2);line-height:1.6;max-width:60ch;}
.idea-new{padding:20px;display:flex;flex-direction:column;gap:8px;margin-bottom:18px;}
.idea-new .fl{font-size:.78rem;font-weight:600;color:var(--label-2);}
.idea-new input,.idea-new textarea{
  width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:var(--r-m);
  background:var(--surface-2);color:var(--ink);font:inherit;resize:vertical;
}
.idea-new input:focus,.idea-new textarea:focus{outline:2px solid var(--accent);outline-offset:1px;}
.idea-new .btn{align-self:flex-start;margin-top:4px;}

.idea-list{display:flex;flex-direction:column;gap:12px;}
.idea{display:flex;gap:14px;padding:16px;position:relative;}
.idea-main{min-width:0;flex:1;}
.idea-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.idea-top h3{font-size:1rem;margin:0;line-height:1.4;}
.idea-body{margin:8px 0 0;color:var(--label-2);line-height:1.65;white-space:pre-wrap;}
/* What staff said back. Marked off with a rule rather than a box: it is a
   reply to the idea, not a second idea. */
.idea-note{
  margin:10px 0 0;padding-inline-start:12px;border-inline-start:3px solid var(--accent);
  color:var(--label-1);line-height:1.6;
}
.idea-by{
  margin:12px 0 0;display:flex;align-items:center;gap:7px;flex-wrap:wrap;
  font-size:.76rem;color:var(--label-3);
}
.av.xs{width:22px;height:22px;font-size:.6rem;}
.idea-by .tag{
  background:var(--accent-soft);color:var(--accent);border-radius:99px;
  padding:1px 8px;font-weight:600;
}
.idea-admin{display:flex;gap:6px;flex-wrap:wrap;margin-top:12px;}
/* Saying something back sits on the same row as the status, because they are
   one action: the status is what happened, the note is why. It wraps to its
   own line on a phone rather than squeezing the buttons. */
.idea-reply{display:flex;gap:6px;flex:1 1 240px;min-width:0;}
.idea-reply input{
  flex:1;min-width:0;padding:6px 11px;border:1px solid var(--line);
  border-radius:var(--r-m);background:var(--surface-2);color:var(--ink);
  font:inherit;font-size:.82rem;
}
.idea-reply input:focus{outline:2px solid var(--accent);outline-offset:1px;}

.idea-del{
  position:absolute;top:10px;inset-inline-end:10px;
  width:26px;height:26px;border:0;border-radius:99px;
  background:transparent;color:var(--label-4);cursor:pointer;font-size:.8rem;
  opacity:0;transition:opacity .15s var(--ease),background .15s var(--ease);
}
.idea:hover .idea-del,.idea-del:focus-visible{opacity:1;}
.idea-del:hover{background:var(--crit-soft);color:var(--crit);}
/* No hover on a phone, so the control has to be there all along. */
@media (pointer:coarse){.idea-del{opacity:1;}}

/* ==========================================================================
   Subscription — plans, paying, and the payments desk
   ========================================================================== */
.sub-pitch{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
  padding:18px 20px;margin-bottom:16px;background:var(--accent-soft);}
.sub-pitch b{font-size:1.02rem;}
.sub-ok{margin:0 0 14px;}
.sub-status{padding:18px 20px;display:flex;flex-direction:column;gap:4px;margin-bottom:6px;}
.sub-status.ok{background:var(--ok-soft);}
.sub-status.ok b{color:var(--ok);}
.sub-pending{padding:18px 20px;background:var(--warn-soft);margin-top:12px;}
.sub-pending b{color:var(--warn);}
.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;}
.plan{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:6px;text-align:start;
  padding:16px 18px;border:2px solid transparent;cursor:pointer;font:inherit;color:inherit;
  transition:border-color .15s var(--ease),background .15s var(--ease);}
.plan[aria-pressed="true"]{border-color:var(--accent);background:var(--accent-soft);}
.plan-n{font-weight:600;font-size:.95rem;}
.plan-p{font-size:.95rem;color:var(--label-2);}
.plan-p b{font-size:1.7rem;color:var(--label);font-family:var(--font-d);}
.plan-tag{position:absolute;inset-inline-end:12px;top:12px;font-size:.7rem;font-weight:700;
  background:var(--ok);color:#fff;padding:3px 8px;border-radius:99px;}
.pm-to{font-family:var(--font-m);font-size:1.15rem;letter-spacing:.02em;}
.pay-send{padding:14px 18px;margin-top:12px;display:flex;flex-direction:column;gap:10px;}
.pay-send p{margin:0;}
.pay-num{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.pay-num code{font-family:var(--font-m);font-size:1.35rem;font-weight:600;letter-spacing:.03em;}
.pay-form{padding:18px;display:flex;flex-direction:column;gap:12px;}
.pay-pick{align-self:flex-start;cursor:pointer;}
.pay-prev{max-width:220px;max-height:260px;border-radius:var(--r-m);margin-top:8px;object-fit:contain;background:var(--surface-2);}
.pay-st{font-size:.76rem;font-weight:600;padding:3px 9px;border-radius:99px;background:var(--surface-3);color:var(--label-2);}
.pay-st.approved{background:var(--ok-soft);color:var(--ok);}
.pay-st.rejected{background:var(--crit-soft);color:var(--crit);}
.pay-st.pending{background:var(--warn-soft);color:var(--warn);}
.pay-row{align-items:flex-start;}
.pay-row .s{display:block;}
.pay-thumb{flex:none;width:56px;height:56px;border-radius:var(--r-s);overflow:hidden;padding:0;border:0;
  background:var(--surface-2);cursor:zoom-in;}
.pay-thumb img{width:100%;height:100%;object-fit:cover;}

/* Two ways to sign up: with a centre ID, or without one (paid). */
.signup-kind{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;margin:0 0 20px;}
@media (max-width:420px){.signup-kind{grid-template-columns:1fr;}}
.signup-kind button{display:flex;flex-direction:column;align-items:flex-start;gap:4px;text-align:start;
  padding:14px;border-radius:var(--r-m);border:2px solid var(--line);background:var(--surface);
  font:inherit;color:inherit;cursor:pointer;transition:border-color .15s var(--ease),background .15s var(--ease);}
.signup-kind button b{font-size:.95rem;}
.signup-kind button span{font-size:.8rem;color:var(--label-2);line-height:1.45;}
.signup-kind button[aria-pressed="true"]{border-color:var(--accent);background:var(--accent-soft);}

/* A section an unpaid student can see but not use yet. */
.nav-a.is-locked,.tab.is-locked{opacity:.55;}
.nav-a.is-locked.on{opacity:1;}
.nav-lock{margin-inline-start:auto;display:inline-flex;color:var(--label-3);}
.nav-lock svg{width:13px;height:13px;}
.locked-page{position:relative;max-width:760px;min-height:420px;}
.locked-ghost{opacity:.45;filter:grayscale(1) blur(1.5px);pointer-events:none;user-select:none;}
.locked-card{position:absolute;inset-inline:max(16px,8%);top:60px;padding:30px 26px;text-align:center;
  display:flex;flex-direction:column;align-items:center;gap:10px;box-shadow:var(--sh-3);}
.locked-card h2{font-size:1.2rem;}
.locked-card .hint{max-width:46ch;line-height:1.6;}
.locked-price{font-weight:600;color:var(--accent);margin:2px 0 8px;}
.locked-ic{position:relative;width:54px;height:54px;border-radius:16px;display:grid;place-items:center;
  background:var(--accent-soft);color:var(--accent);}
.locked-ic svg{width:24px;height:24px;}
.locked-ic i{position:absolute;inset-inline-end:-6px;bottom:-6px;width:24px;height:24px;border-radius:50%;
  display:grid;place-items:center;background:var(--label);color:var(--surface);}
.locked-ic i svg{width:12px;height:12px;}

/* --- UI round: payments filter ------------------------------------- */
.pay-tabs{display:flex;max-width:100%;overflow-x:auto;scrollbar-width:none;margin-bottom:14px;}
.pay-tabs::-webkit-scrollbar{display:none;}
.pay-tabs button{flex:1 1 auto;white-space:nowrap;padding-inline:8px;}
@media (max-width:420px){.pay-tabs button{font-size:.8rem;padding-inline:6px;}}

/* --- Subscription students (developer only) ------------------------- */
.subs-tiles{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin-bottom:18px;}
.sub-tile{text-align:start;font:inherit;color:inherit;border:2px solid transparent;cursor:pointer;}
.sub-tile.plain{cursor:default;}
.sub-tile .v{font-size:1.8rem;}
.sub-tile.on{border-color:var(--accent);}
.subs-tools{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
.subs-tools .pay-tabs{margin:0;}
.subs-search{flex:1;min-width:200px;position:relative;display:flex;align-items:center;}
.subs-search svg{position:absolute;inset-inline-start:12px;color:var(--label-3);}
.subs-search .input{padding-inline-start:34px;}
.subs-row{align-items:center;flex-wrap:wrap;}
.subs-row .av{position:relative;flex:none;}
.subs-on{position:absolute;inset-inline-end:-1px;bottom:-1px;width:11px;height:11px;border-radius:50%;
  background:var(--ok);border:2px solid var(--surface);}
.subs-who{flex:1;min-width:220px;display:flex;flex-direction:column;gap:2px;}
.subs-who .s{display:block;}
.subs-period{flex:0 0 230px;display:flex;flex-direction:column;gap:6px;}
@media (max-width:640px){.subs-period{flex-basis:100%;padding-inline-start:52px;}}
.subs-top{display:flex;align-items:center;gap:8px;justify-content:space-between;}
.subs-top b{font-size:.9rem;}
.sub-badge{font-size:.72rem;font-weight:600;padding:3px 9px;border-radius:99px;background:var(--surface-3);color:var(--label-2);}
.sub-badge.active{background:var(--ok-soft);color:var(--ok);}
.sub-badge.soon{background:var(--warn-soft);color:var(--warn);}
.sub-badge.expired{background:var(--crit-soft);color:var(--crit);}
.subs-bar{height:6px;border-radius:99px;background:var(--surface-3);overflow:hidden;}
.subs-bar i{display:block;height:100%;border-radius:inherit;background:var(--ok);}
.subs-bar.soon i{background:var(--warn);}
