/* ============================================================================
   WARCOX design system — the tokens and primitives the signed-in surfaces share.
   Used by: / (landing), /internal, /student, /roster, /settings/students,
   /settings/courses.

   Deliberately CSS only, with no behaviour in it. The pages each keep their own
   JavaScript inline and share none of it, so a bug on one cannot reach the
   others; a stylesheet is the one thing that is safe to have in common, and
   having it in common is what keeps the surfaces looking like one product.

   Values are the established palette and type stack, unchanged: the same three
   Google fonts each page imports, and the same colours the earlier pages use.
   Not modified here: sparring.html, feedback.html and settings/personas.html,
   which carry their own copies of these values inline.
   ============================================================================ */

:root{
  --bg:#0b0a0a; --bg-2:#141212; --surface:#181616; --surface-2:#211d1d;
  --border:#332c2c; --border-soft:#241f1f; --text:#f3efe9; --text-dim:#b8afa7;
  --text-faint:#726963; --accent:#e0182f; --accent-bright:#ff2f45;
  --accent-dim:#5c0d16; --accent-wash:#2a0b0f; --good:#3fae6f; --warn:#d99a2b;
  --display:'Anton',sans-serif;

  /* The palette's deepest black. --bg is the page floor for the data surfaces, which
     want a little lift so borders and dividers read; --bg-deep is for the marketing
     side, where the frame is supposed to fall away into nothing. Separate token rather
     than a darkened --bg so a rebrand can move the two independently. */
  --bg-deep:#050505;

  /* Channel triplets for the same colours above. A hex token cannot be dropped inside
     rgba(), so anything that needs a custom alpha has historically had to re-type the
     channels as a literal - which is exactly the hardcoding a white-label rebrand pass
     would have to hunt down. These let alpha variants go through the token system:
     rgb(var(--accent-rgb) / .13). Keep each triplet in sync with its hex above. */
  --bg-rgb:11 10 10;
  --bg-2-rgb:20 18 18;
  --bg-deep-rgb:5 5 5;
  --accent-rgb:224 24 47;
  --accent-bright-rgb:255 47 69;

  /* Film grain, as a token so every surface that needs it gets the same one rather than
     its own hand-rolled noise. Inline SVG so it costs no request: fractal noise at a
     frequency fine enough to read as emulsion grain, not as a visible pattern. Layer it
     over imagery with mix-blend-mode:overlay at a low opacity. */
  --grain:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}

*{box-sizing:border-box;}

/* The red glow: a wide, low-opacity accent radial over the neutral one, sitting
   behind everything at the top of the page. Same construction as the standings
   and field-manual pages — two stacked gradients on the body, no extra element. */
body{
  margin:0; min-height:100vh; color:var(--text);
  font-family:'Work Sans', system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing:antialiased;
  background:
    radial-gradient(900px 420px at 50% -8%, rgb(var(--accent-rgb) / .13), rgb(var(--accent-rgb) / 0) 70%),
    radial-gradient(1200px 600px at 50% -10%, var(--bg-2), var(--bg) 60%);
  background-color:var(--bg);
  background-repeat:no-repeat;
  background-attachment:fixed;
}

[hidden]{display:none!important;}

.wrap{max-width:1080px;margin:0 auto;padding:0 24px 72px;}
.wrap.narrow{max-width:480px;}
.wrap.mid{max-width:760px;}

/* ---- header ---- */
.site-head{
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:22px 0 18px;border-bottom:1px solid var(--border-soft);margin-bottom:30px;
}
.brand-word{font-family:'Anton',sans-serif;letter-spacing:.04em;font-size:17px;text-decoration:none;color:var(--text);}
.tag{
  font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.18em;
  color:var(--text-faint);text-transform:uppercase;
}
.head-right{display:flex;align-items:center;gap:14px;}
.who{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--text-faint);}

/* ---- type ---- */
h1{font-family:'Anton',sans-serif;font-weight:400;font-size:clamp(28px,6vw,38px);margin:0 0 6px;text-wrap:balance;}
h2{font-family:'Anton',sans-serif;font-weight:400;font-size:clamp(20px,3.4vw,25px);margin:0 0 12px;letter-spacing:.01em;}
h1 span,h2 span{color:var(--accent-bright);}
p.lede{color:var(--text-dim);font-size:14.5px;line-height:1.55;margin:0 0 26px;}
.eyebrow{
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--text-faint);margin:0 0 12px;
}

/* ---- forms ---- */
.field-label{
  display:block;font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);margin:16px 0 7px;
}
input[type=text],input[type=email],input[type=password],input[type=url],select,textarea{
  width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);
  font-family:'Work Sans',sans-serif;font-size:14.5px;padding:12px 13px;border-radius:9px;
}
textarea{resize:vertical;min-height:84px;line-height:1.5;}
input:focus,select:focus,textarea:focus,button:focus-visible,a:focus-visible{
  outline:2px solid var(--accent-bright);outline-offset:2px;
}

/* ---- the eye on a password field ----
   A password nobody can read is a password typed wrong -- on a phone keyboard especially --
   so a field that takes one carries a reveal control. Three things about how it is built:

   It is a real <button>, not an icon with a click handler on it. The control has to be
   reachable by keyboard and it has to announce what it is and which way it is set, and
   aria-pressed on a button is the one construction that gets all of that for free.

   The wrapper is deliberately inert: display:block, no border, no padding, no margin. It
   exists only to be the positioning context for the button, so the field's own box, and the
   margins collapsing around it, are to the pixel what they were before the wrapper was
   there. Nothing in the rule above is overridden except the right padding, which keeps the
   last character of a long password clear of the icon.

   Both icons ship in the markup and CSS picks between them off aria-pressed -- the same
   attribute the assistive technology reads -- so what is drawn and what is announced cannot
   drift apart, and toggling costs no DOM work. Masked shows the slashed eye (the password is
   hidden); revealed shows the open one. */
.pw-field{position:relative;display:block;}
.pw-field input{padding-right:44px;}
/* Edge paints its own reveal button inside a password input. One eye, not two. */
.pw-field input::-ms-reveal,.pw-field input::-ms-clear{display:none;}
.pw-reveal{
  position:absolute;top:0;right:0;height:100%;width:42px;
  display:flex;align-items:center;justify-content:center;
  background:none;border:0;padding:0;margin:0;cursor:pointer;color:var(--text-faint);
}
.pw-reveal:hover{color:var(--text-dim);}
/* Flush to the field's edge, so the focus ring is drawn just inside it rather than
   half-clipped by the corner it would otherwise sit outside. */
.pw-reveal:focus-visible{outline-offset:-4px;border-radius:9px;}
.pw-reveal svg{width:19px;height:19px;display:block;}
.pw-reveal .pw-eye{display:none;}
.pw-reveal[aria-pressed=true] .pw-eye{display:block;}
.pw-reveal[aria-pressed=true] .pw-eye-off{display:none;}

.btn{
  width:100%;background:var(--accent);color:#fff;border:none;border-radius:9px;padding:14px;
  font-family:'Anton',sans-serif;font-size:15px;letter-spacing:.02em;cursor:pointer;margin-top:22px;
}
.btn:hover{background:var(--accent-bright);}
.btn:disabled{background:var(--surface-2);color:var(--text-faint);cursor:not-allowed;}
.btn-ghost{background:transparent;border:1px solid var(--border);color:var(--text-dim);margin-top:10px;}
.btn-ghost:hover{background:var(--surface);color:var(--text);}
/* The destructive button. Outlined rather than filled, because the filled .btn is already the
   accent red and a permanent action must not look like the ordinary one it sits next to -- it
   fills in on hover, at the moment of the press. Disabled inherits .btn:disabled above, which
   is what the delete confirmation leans on while the name is being typed. */
.btn-danger{
  background:transparent;border:1px solid var(--accent);color:var(--accent-bright);
}
.btn-danger:hover:not(:disabled){background:var(--accent);color:#fff;}
.btn-sm{width:auto;padding:9px 15px;font-size:13px;margin:0;}

.switcher{display:flex;gap:6px;background:var(--surface-2);border-radius:9px;padding:4px;margin-bottom:8px;}
.switcher button{
  flex:1;background:transparent;border:none;color:var(--text-dim);font-family:'Work Sans',sans-serif;
  font-weight:600;font-size:13px;padding:9px;border-radius:6px;cursor:pointer;
}
.switcher button.active{background:var(--accent);color:#fff;}

/* ---- cards ---- */
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;margin-bottom:14px;}
.card h3{margin:0 0 4px;font-family:'Anton',sans-serif;font-weight:400;font-size:16px;}
.card p{margin:0;color:var(--text-dim);font-size:13px;line-height:1.5;}
a.card{display:block;text-decoration:none;color:inherit;}
a.card:hover{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);}

/* Two-up choice cards — the Sales/Delivery and Setter/Closer pickers.
   `.pick` sets `display` on whatever carries it, so it is a layout claim on a bare,
   tempting word: anything else that wants a "pickable" class needs its own prefixed
   name. The Trackers sheet's clickable day rows learned that the hard way — as `pick`
   they stopped being table rows and their numbers left their columns. */
.pick{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}
.pick-card{
  background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px;
  cursor:pointer;text-align:left;color:var(--text);font-family:'Work Sans',sans-serif;
}
.pick-card:hover{border-color:var(--accent-dim);}
.pick-card.selected{border-color:var(--accent-bright);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);}
.pick-card .pn{font-family:'Anton',sans-serif;font-size:16px;}
.pick-card .pd{font-size:11.5px;color:var(--text-dim);margin-top:4px;line-height:1.4;}
@media (max-width:520px){ .pick{grid-template-columns:1fr;} }

/* ---- pills, tags, messages ---- */
.pill{
  display:inline-block;font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--accent-bright);border:1px solid var(--accent-dim);
  background:var(--accent-wash);padding:3px 9px;border-radius:99px;
}
.pill.muted{color:var(--text-faint);border-color:var(--border);background:var(--surface-2);}
.pill.good{color:var(--good);border-color:#1f5c3b;background:#0d2118;}
.pill.warn{color:var(--warn);border-color:#5c4413;background:#221a0b;}

.msg{margin-top:14px;padding:11px 13px;border-radius:9px;font-size:13px;line-height:1.45;}
.msg.err{border:1px solid var(--accent-dim);background:var(--accent-wash);color:#ffd2d8;}
.msg.ok{border:1px solid #1f5c3b;background:#0d2118;color:#bdf0d3;}
.msg.warn{border:1px solid #5c4413;background:#221a0b;color:#f0dcae;}
.msg.info{border:1px solid var(--border);background:var(--surface-2);color:var(--text-dim);}

/* ---- tab navigation ----
   The sections of a page are chips with air between them rather than words sharing an
   underline. There are two ways they are laid out, and which one a page wants is decided by
   how many chips it has:

     the strip — this block. One row that scrolls sideways. For a handful of chips: /student's
                 eight, the Agreements panel's two.
     the board — `.tab-grid` / `.tab-groups` further down. A wrapping grid, grouped, with no
                 scroll in it at all. For /internal's eighteen, where a row means most of the
                 dashboard's sections are off screen.

   The chip itself — `.tab` — is shared by both, so the two layouts are two arrangements of one
   control and not two controls.

   The strip scrolls sideways instead of wrapping. `flex-wrap:nowrap` with `overflow-x:auto` is
   the whole mechanism; the scrollbar is hidden because the chips themselves are the
   affordance, and /assets/tab-rail.js adds the fade at whichever edge still has strip behind
   it so it is never a guess whether there is more. That module also pulls the active chip
   into view, which matters on a phone where the tab a hash names can start off screen.

   Every chip is at least 44px tall — the tap target Apple and Google both ask for — and the
   active one is filled with the accent rather than underlined with it, the same way
   `.switcher button.active` reads. Nothing here overlaps at any width: one row, no wrap, no
   negative margins, and horizontal scroll rather than a squeeze. */
.tabs{
  display:flex;flex-wrap:nowrap;align-items:stretch;gap:8px;
  overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;
  scroll-snap-type:x proximity;overscroll-behavior-x:contain;
  /* The strip is its own scroll container, so the focus ring of a chip at either end would be
     clipped by the overflow. The vertical padding is that ring's room, and the horizontal
     padding is the room the active chip's shadow needs. */
  padding:5px 3px 8px;margin:0 0 24px;
  scrollbar-width:none;-ms-overflow-style:none;
}
.tabs::-webkit-scrollbar{display:none;}

/* Which edge has strip behind it, set by /assets/tab-rail.js. Without that module on the page
   neither class is ever added and the strip renders unmasked — exactly as it did before. */
.tabs.rail-start{
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 26px);
  mask-image:linear-gradient(90deg,transparent 0,#000 26px);
}
.tabs.rail-end{
  -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent 100%);
}
.tabs.rail-start.rail-end{
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 26px,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(90deg,transparent 0,#000 26px,#000 calc(100% - 26px),transparent 100%);
}

.tab{
  flex:0 0 auto;scroll-snap-align:start;
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:44px;padding:11px 17px;border-radius:11px;
  background:var(--surface);border:1px solid var(--border);color:var(--text-dim);
  font-family:'Work Sans',sans-serif;font-weight:700;font-size:13.5px;letter-spacing:.005em;
  white-space:nowrap;text-decoration:none;cursor:pointer;
  transition:background .14s ease,border-color .14s ease,color .14s ease;
}
.tab:hover{background:var(--surface-2);border-color:var(--accent-dim);color:var(--text);}
/* Filled, not underlined: on a strip that scrolls, the chip you are on has to be findable at a
   glance from wherever the strip happens to be sitting. */
.tab.active{
  background:var(--accent);border-color:var(--accent-bright);color:#fff;
  box-shadow:0 8px 20px -12px rgb(var(--accent-rgb) / .9);
}
.tab.active:hover{background:var(--accent-bright);border-color:var(--accent-bright);color:#fff;}
/* Locked tabs stay visible and clickable — clicking explains why, which is more use
   than a tab that cannot be pressed at all. */
.tab.locked{color:var(--text-faint);}
.tab.locked:hover{color:var(--text-dim);}
.tab .lock{font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.1em;opacity:.75;}
/* Unread direct messages, on the Members tab. Sits in the tab strip beside `.lock` because
   they are the same idea from opposite directions: one says a tab has nothing for you yet, and
   this one says it has something waiting. */
.tab .tab-count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:18px;height:18px;padding:0 5px;margin-left:0;border-radius:99px;
  background:var(--accent);color:#fff;
  font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:600;letter-spacing:0;
}
/* On the filled chip the accent badge would disappear into its own background. */
.tab.active .tab-count{background:#fff;color:var(--accent);}

/* A phone holds three or four chips at a time and the rest are a swipe away, so they are
   sized for a thumb rather than shrunk to fit more of them on. */
@media (max-width:640px){
  /* Edge to edge on a phone: the negative margin cancels .wrap's 24px and the padding puts it
     back inside the strip, so the first chip still lines up with the page's text while the
     strip itself runs off both sides — which is what makes it read as swipeable, and where the
     last chip's other half goes instead of being cut against a margin. The two numbers are one
     number: change .wrap's padding and change these. */
  .tabs{
    gap:7px;margin:0 -24px 20px;padding:5px 24px 8px;
    scroll-padding-left:24px;
  }
  .tab{min-height:46px;padding:12px 16px;font-size:13px;border-radius:12px;}
}

/* A thumb swipes the strip and a trackpad shrugs sideways, but a mouse with one wheel has
   neither — so a mouse gets a slim bar to drag, and only a mouse. Keyboards were never
   relying on it: focus moves along the strip with Tab and the browser scrolls to keep up. */
@media (hover:hover) and (pointer:fine){
  .tabs{scrollbar-width:thin;scrollbar-color:var(--border) transparent;}
  .tabs::-webkit-scrollbar{display:block;height:6px;}
  .tabs::-webkit-scrollbar-track{background:transparent;}
  .tabs::-webkit-scrollbar-thumb{background:var(--border);border-radius:99px;}
  .tabs:hover::-webkit-scrollbar-thumb{background:var(--text-faint);}
}

/* ---- the tab board ----
   The other layout for the same chips, and the one /internal's seat strip uses: a wrapping
   grid instead of a scrolling row.

   Why both exist. A scrolling row is the right answer for a handful of chips — /student's
   eight, the Agreements panel's two — because it costs one line of page height and the swipe
   is cheap. It is the wrong answer for eighteen: everything past the first three or four is
   off screen, so reaching the Book of Business meant swiping past nine tabs to find out
   whether it was there at all, and a section list you have to scroll to read is a section list
   nobody reads. The dashboard is the page with eighteen, so the dashboard gets the board.

   The mechanism is one line: `repeat(auto-fill, minmax(--tab-min, 1fr))`. Every group below is
   its own grid with the same `--tab-min` and the same available width, so every group resolves
   to the same column count and the chips line up down the page as one grid rather than as five
   ragged rows. Change the number, not the layout: the column count is whatever the viewport
   affords — five on a 1080px page, two on a 320px phone — and nothing here has a breakpoint
   that knows how many tabs there are.

   Chips are bigger here than on the scroller, and can be: nothing is competing for the row.
   58px tall against 44px, and wide enough for two lines of label, because `white-space:normal`
   is what keeps a long name inside its own cell instead of overhanging the chip next to it. A
   grid row stretches to its tallest chip, so a two-line chip does not leave its neighbours
   half height.

   Grouping is a label and a hairline and nothing else — no accordion, no "more" chip, no
   second click. Every tab the account has is on screen the moment the page paints; the groups
   are only there so eighteen chips read as five short lists rather than as one wall. */
.tab-grid{
  --tab-min:170px;
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(var(--tab-min),1fr));
  gap:8px;align-items:stretch;margin:0;
}
/* As a strip in its own right rather than one group of a board: the trailing space the
   scrolling `.tabs` carries, so the panel below it does not start against the chips. */
nav.tab-grid{margin:0 0 26px;}

/* The board: the groups, stacked. Wider gap between groups than between the chips inside one,
   which with the labels is the whole of the visual separation — no boxes, no cards, nothing
   that would make five groups read as five panels. */
.tab-groups{display:grid;gap:18px;margin:0 0 26px;}

/* The group's name, with the rule that separates it running out to the right edge. One element
   for both: a divider drawn as a second node is a divider that can drift away from its label.
   Same mono/uppercase/tracking as `.eyebrow` and `.tag`, because it is the same idea. */
.tab-group-label{
  display:flex;align-items:center;gap:12px;
  font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:500;letter-spacing:.2em;
  text-transform:uppercase;color:var(--text-faint);
  margin:0 0 9px;
}
.tab-group-label::after{content:"";flex:1 1 auto;height:1px;background:var(--border-soft);}
/* The group holding the tab you are on, said quietly. The chip itself is filled with the
   accent and is what actually answers "where am I"; this only helps the eye find which of the
   five short lists that chip is in. Unsupported `:has()` costs the hint and nothing else. */
.tab-group:has(.tab.active) .tab-group-label{color:var(--text-dim);}

/* A chip on the board. Everything above in `.tab` still applies — the surface, the border, the
   accent fill when active, the count badge — and this changes size and wrapping only, so the
   dashboard's tabs and every other strip in the app remain recognisably the same control. */
.tab-grid .tab{
  width:100%;min-height:58px;padding:12px 14px;
  border-radius:12px;font-size:14px;line-height:1.25;
  white-space:normal;text-align:center;flex-wrap:wrap;
  scroll-snap-align:none;
}

/* Between a phone and a full page the minimum comes down, because at 170px a 700px-wide window
   resolves to three columns and the tallest group needs two rows to hold five chips. 150px
   keeps it at four across that whole band. Above it the wider minimum is the better trade: the
   biggest group is five tabs, so a sixth column would buy no height back and would only make
   every chip narrower. */
@media (min-width:641px) and (max-width:899px){
  .tab-grid{--tab-min:150px;}
}

/* A phone gets two columns rather than one. 132px is the widest minimum that still resolves to
   two of them inside `.wrap`'s margins on a 320px screen, which is the narrowest phone anyone
   is on — one column there would be eighteen full-width rows to scroll past. The chips stay
   tall enough for a thumb; it is the label that wraps, not the target that shrinks. */
@media (max-width:640px){
  .tab-grid{--tab-min:132px;gap:7px;}
  nav.tab-grid{margin:0 0 20px;}
  .tab-groups{gap:16px;margin:0 0 20px;}
  .tab-grid .tab{min-height:56px;padding:11px 10px;font-size:13.5px;}
  .tab-group-label{margin:0 0 8px;}
}

/* ---- the welcome header ----
   The one line above the Discussion Board on both dashboards: the account's photo, its seat or
   status, and its own name. It belongs to that panel and to no other — the pages hide it with
   the `hidden` attribute when any other tab is on, and `[hidden]{display:none!important}` at
   the top of this file means the row leaves no gap behind when it goes.

   The photo here is a photo and nothing else. Setting one is an account setting and lives on
   the profile sheet behind the header photo, so there is no picker, no camera and no "change
   photo" button on the page somebody reads the board on. */
.welcome{
  display:flex;align-items:center;gap:15px;
  margin:0 0 22px;padding-bottom:18px;border-bottom:1px solid var(--border-soft);
}
.welcome-face{flex:0 0 auto;line-height:0;}
.welcome-main{flex:1 1 auto;min-width:0;}
.welcome-main .pill{display:inline-block;margin:0 0 9px;}
.welcome-hi{
  font-family:'Anton',sans-serif;font-weight:400;font-size:clamp(26px,5.4vw,36px);
  line-height:1.05;margin:0;text-wrap:balance;overflow-wrap:anywhere;
}
.welcome-hi span{color:var(--accent-bright);}
.welcome-note{color:var(--text-dim);font-size:13.5px;line-height:1.5;margin:8px 0 0;}
@media (max-width:520px){
  .welcome{gap:13px;padding-bottom:16px;margin-bottom:18px;}
  .welcome .avatar.avatar-lg{width:54px;height:54px;}
  .welcome .avatar.avatar-lg .av-mark{font-size:19px;}
  .welcome-note{font-size:13px;}
}

/* ---- data tables ---- */
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.data{width:100%;border-collapse:collapse;font-size:13.5px;}
table.data th{
  text-align:left;font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);font-weight:500;
  padding:0 12px 9px 0;border-bottom:1px solid var(--border);white-space:nowrap;
}
table.data td{padding:12px 12px 12px 0;border-bottom:1px solid var(--border-soft);vertical-align:top;}
table.data tr:last-child td{border-bottom:none;}
table.data td.num{font-family:'IBM Plex Mono',monospace;white-space:nowrap;}
table.data td.date{font-family:'IBM Plex Mono',monospace;color:var(--text-dim);white-space:nowrap;}
/* A cell whose text is a link to somewhere. A <button> rather than an <a> because the target
   is a panel on the same page, not a URL — so it inherits none of a button's chrome and all of
   the cell's type. Used by the Book of Business for a client who has a platform account; the
   clients who do not stay as plain text in the same cell, which is why the underline is what
   distinguishes them. */
.link-cell{
  background:none;border:0;padding:0;margin:0;cursor:pointer;text-align:left;
  font:inherit;color:inherit;
  text-decoration:underline;text-decoration-color:var(--accent-dim);text-underline-offset:3px;
}
.link-cell:hover{color:var(--accent);text-decoration-color:var(--accent);}
.strong{font-weight:600;}

/* ---- summary strip ---- */
.stats{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px;}
.stat{
  flex:1 1 150px;background:var(--surface);border:1px solid var(--border);
  border-radius:11px;padding:14px 16px;
}
.stat .sl{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.15em;
  text-transform:uppercase;color:var(--text-faint);
}
.stat .sv{font-family:'Anton',sans-serif;font-size:24px;margin-top:5px;letter-spacing:.01em;}

/* ---- empty states ---- */
.empty{
  border:1px dashed var(--border);border-radius:12px;padding:44px 24px;text-align:center;
  background:rgba(24,22,22,.45);
}
.empty .ei{font-family:'Anton',sans-serif;font-size:19px;margin-bottom:8px;}
.empty p{margin:0 auto;max-width:44ch;color:var(--text-dim);font-size:13.5px;line-height:1.55;}

.loading{color:var(--text-faint);font-family:'IBM Plex Mono',monospace;font-size:12px;padding:22px 0;}

/* ---- calendar: the Day / Week / Month views of the recurring schedule ----
   One set of primitives for all three, because they are three renderings of the same rows.
   An event is a button everywhere it appears: clicking it asks the server for its details,
   and a locked one is greyed but still pressable — that press is how a student finds out
   what opens it, so it must never be disabled. */

.cal-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px;}
/* The mode switcher sizes to its three words rather than stretching, so it reads as a control
   rather than as a header band. */
.switcher.cal-modes{margin:0;flex:0 0 auto;}
.switcher.cal-modes button{flex:0 0 auto;padding:8px 16px;}
.cal-nav{display:flex;align-items:center;gap:6px;}
.cal-nav .btn{margin:0;}
.cal-step{
  width:34px;padding:8px 0;font-family:'IBM Plex Mono',monospace;font-size:14px;line-height:1;
}
.cal-range{
  font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--text-dim);margin-bottom:12px;
}
.cal-range b{color:var(--text);font-weight:600;}

/* ---- week ---- */
.week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px;}
.day{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:11px 10px;min-height:132px;}
.day.today{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 60%);}
.day-name{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--text-faint);margin-bottom:9px;display:flex;justify-content:space-between;align-items:baseline;gap:6px;
}
.day.today .day-name{color:var(--accent-bright);}
.day-none{font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--text-faint);opacity:.6;}

/* ---- one event, as it appears inside a week or day cell ---- */
.ev{
  display:block;width:100%;text-align:left;background:transparent;border:none;
  border-left:3px solid var(--border);border-radius:0 6px 6px 0;color:var(--text);
  font-family:'Work Sans',sans-serif;padding:2px 6px 3px 8px;margin-bottom:10px;cursor:pointer;
}
.ev:last-child{margin-bottom:0;}
.ev:hover{background:var(--surface-2);}
.ev.selected{background:var(--accent-wash);}
.ev-t{display:block;font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--text-dim);}
.ev-n{display:block;font-size:12px;font-weight:600;line-height:1.3;margin-top:2px;}
.ev-d{display:block;font-family:'IBM Plex Mono',monospace;font-size:9.5px;color:var(--text-faint);margin-top:2px;}
/* Greyed out, in the same register as a locked opponent in the Sparring Room: the session is
   still legible, it just visibly isn't yours yet. */
.ev.locked{border-left-color:var(--border-soft);}
.ev.locked .ev-t,.ev.locked .ev-n,.ev.locked .ev-d{color:var(--text-faint);}
.ev.locked .ev-n{font-weight:500;}
.ev-lock{
  display:block;font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--warn);opacity:.85;margin-top:3px;
}

/* ---- a session's cover, and the Join control ----

   The row wraps the card because a session can carry two controls and one of them is a link: an
   <a> cannot live inside a <button>, so the Join anchor is the button's sibling. The margin
   moves onto the wrapper for the same reason — `.ev:last-child` stopped being the last thing in
   the cell the moment the anchor appeared next to it.

   `.has-join` is on the row only when the listing actually carried a link, which is only ever on
   a session this account has been gated open. It is what buys the day card the room on its right
   for the chip; a card with no Join keeps its full width, so nothing is reserved for a control
   that is not there.

   The cover itself is `.cover`, unchanged and shared with the course cards, which is the point:
   one treatment, one placeholder, one grain. Only the size is set here. It is drawn in the day
   list and on the detail card and deliberately not in the week or month grid — a seventh of the
   panel with five sessions stacked in it has no room for five pictures. */
.ev-row{margin-bottom:10px;}
.ev-row:last-child{margin-bottom:0;}
.ev-row .ev{margin-bottom:0;}
.ev-join{
  display:inline-block;margin:6px 0 0 8px;background:var(--accent);color:#fff;text-decoration:none;
  font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.12em;text-transform:uppercase;
  padding:5px 10px;border-radius:6px;
}
.ev-join:hover{background:var(--accent-bright);}
.ev-join:focus-visible{outline:2px solid var(--accent-bright);outline-offset:2px;}
.ev-cover{flex:0 0 108px;width:108px;border-radius:8px;}
/* The placeholder's initials are set for a course card three times this wide, and 30px in a
   108px frame overflows it. This is not an edge case today — no session has a picture yet, so
   the placeholder is what every card is currently wearing. */
.ev-cover .cover-mark{font-size:16px;}
.ev:hover .ev-cover{filter:brightness(1.08);}
/* Desaturated rather than hidden, in the same register `.course-card.locked .cover` gives a
   course the student has not reached: the picture is still there, it visibly isn't theirs. */
.ev.locked .ev-cover{filter:grayscale(1) brightness(.6);}
.ev.locked:hover .ev-cover{filter:grayscale(1) brightness(.6);}

/* ---- day ---- */
.daylist{display:grid;gap:10px;}
.daylist .ev{
  background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--border);
  border-radius:11px;padding:14px 16px;margin:0;
}
.daylist .ev:hover{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);}
.daylist .ev.locked{background:var(--bg-2);border-color:var(--border-soft);}
.daylist .ev.locked:hover{background:var(--surface);border-color:var(--border);}
.daylist .ev-t{font-size:12px;letter-spacing:.06em;}
.daylist .ev-n{font-size:15px;font-family:'Anton',sans-serif;font-weight:400;margin-top:5px;}
.daylist .ev-d{font-size:10.5px;margin-top:4px;}
/* The day view is the one list of full-width cards on this panel, so it is the one that gets the
   picture: cover on the left, the slot and the title on the right, Join in the card's own top
   corner. `.daylist` already spaces its children with `gap`, so the row's margin comes off. */
.daylist .ev-row{margin:0;position:relative;}
.daylist .ev{display:flex;align-items:center;gap:14px;}
.daylist .ev-main{flex:1;min-width:0;}
.daylist .ev-row.has-join .ev{padding-right:96px;}
.daylist .ev-join{
  position:absolute;top:14px;right:16px;margin:0;font-size:10px;padding:7px 14px;border-radius:7px;
}

/* ---- month ---- */
.month{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;}
.month-head{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--text-faint);text-align:center;padding:0 0 2px;
}
.mday{
  background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:7px 6px;
  min-height:96px;display:flex;flex-direction:column;gap:4px;
}
/* Days either side of the month stay on the grid so the weeks line up, but recede. */
.mday.out{background:var(--bg-2);border-color:var(--border-soft);}
.mday.out .mnum{color:var(--text-faint);opacity:.55;}
.mday.today{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 60%);}
.mnum{font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--text-dim);text-align:right;}
.mday.today .mnum{color:var(--accent-bright);font-weight:600;}
.mchip{
  display:block;width:100%;text-align:left;background:var(--surface-2);border:1px solid transparent;
  border-left:3px solid var(--border);border-radius:5px;color:var(--text);
  font-family:'Work Sans',sans-serif;font-size:10.5px;line-height:1.25;padding:3px 5px;cursor:pointer;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.mchip:hover{border-color:var(--accent-dim);}
.mchip .mct{font-family:'IBM Plex Mono',monospace;color:var(--text-dim);margin-right:4px;}
.mchip.locked{background:transparent;border-left-color:var(--border-soft);color:var(--text-faint);}
.mchip.locked .mct{color:var(--text-faint);opacity:.8;}
.mchip.selected{border-color:var(--accent-bright);}

/* ---- the detail card, filled by the server on click ---- */
.evd{
  background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;margin-top:16px;
}
.evd.open{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);}
.evd.shut{background:var(--bg-2);border-color:var(--border-soft);}
.evd-eyebrow{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--text-faint);margin-bottom:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;
}
.evd.shut .evd-eyebrow{color:var(--warn);}
.evd h3{margin:0 0 8px;font-family:'Anton',sans-serif;font-weight:400;font-size:19px;}
.evd-when{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--text-dim);}
.evd-body{margin:12px 0 0;color:var(--text-dim);font-size:13.5px;line-height:1.55;white-space:pre-wrap;}
.evd-note{margin:12px 0 0;color:var(--text-faint);font-size:12.5px;line-height:1.5;}
.evd-join{
  display:inline-block;margin-top:16px;background:var(--accent);color:#fff;text-decoration:none;
  font-family:'Anton',sans-serif;font-size:14px;letter-spacing:.02em;padding:11px 18px;border-radius:9px;
}
.evd-join:hover{background:var(--accent-bright);}
.evd-close{
  background:transparent;border:none;color:var(--text-faint);font-family:'IBM Plex Mono',monospace;
  font-size:10px;letter-spacing:.12em;text-transform:uppercase;cursor:pointer;padding:0;margin-left:auto;
}
.evd-close:hover{color:var(--text);}
/* Full width and 16:9 on the detail card — the course card's own proportions, on the one surface
   here with the room for them. `.cover` already clips its <img>, so the radius is all this adds. */
.evd-cover{width:100%;border-radius:9px;margin-bottom:14px;}

@media (max-width:900px){
  .week{grid-template-columns:1fr;}
  .day{min-height:0;}
  .month{gap:4px;}
  .mday{min-height:74px;padding:5px 4px;}
  .mchip{font-size:9.5px;}
  .mchip .mct{display:none;}
}
@media (max-width:560px){
  .cal-bar{gap:8px;}
  .switcher.cal-modes{flex:1 1 100%;}
  .switcher.cal-modes button{flex:1 1 0;}
  .cal-nav{flex:1 1 100%;justify-content:space-between;}
  /* On a phone the day card stacks: the cover becomes a banner across the top, and the Join chip
     comes out of the corner and back into the flow, because 96px of reserved right-hand padding
     on a 320px card leaves nowhere for a session's name. */
  .daylist .ev{display:block;}
  /* Full width on a phone, so the initials go back to the size the frame can carry. */
  .daylist .ev .ev-cover{flex:none;width:100%;margin-bottom:11px;}
  .daylist .ev .ev-cover .cover-mark{font-size:30px;}
  .daylist .ev-row.has-join .ev{padding-right:16px;}
  .daylist .ev-row{position:static;}
  .daylist .ev-join{position:static;margin:8px 0 0 16px;}
}

/* ---- the unlock callout ----
   What a reader sees after pressing a session they have not earned. It sits above the calendar
   and is built to read as the instruction it is: the warn tint and the padlock mark it as a
   gate rather than a fault, and the one sentence in it is the server's own — including, where
   the gate is a course, a link straight to that course in the Training tab.

   Here rather than inline on either dashboard because the calendar is one panel on two of
   them: /assets/calendar.js draws this markup for /student and /internal alike. Same tokens as
   the practical banner on /student, in the warn family rather than the accent one, because this
   is a "here is what opens it" and not an announcement. */
.unlock{
  display:flex;gap:13px;align-items:flex-start;
  border:1px solid #5c4413;border-radius:12px;padding:15px 16px;margin-bottom:16px;
  background:linear-gradient(180deg,#221a0b,var(--surface) 85%);
}
.unlock-icon{
  flex:0 0 auto;width:34px;height:34px;border-radius:9px;display:flex;
  align-items:center;justify-content:center;background:rgba(217,154,43,.13);
  border:1px solid rgba(217,154,43,.3);
}
.unlock-icon svg{width:17px;height:17px;fill:var(--warn);display:block;}
.unlock-main{flex:1 1 auto;min-width:0;}
.unlock-eyebrow{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--warn);
}
.unlock-head{font-family:'Anton',sans-serif;font-size:18px;margin:7px 0 5px;}
.unlock-when{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--text-faint);}
.unlock-body{margin:9px 0 0;color:var(--text-dim);font-size:13.5px;line-height:1.55;}
.unlock-body a{
  color:#f0dcae;font-weight:600;text-decoration:underline;text-underline-offset:2px;
  text-decoration-thickness:1px;cursor:pointer;
}
.unlock-body a:hover{color:#fff;}
.unlock-close{
  flex:0 0 auto;background:transparent;border:none;color:var(--text-faint);
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.12em;
  text-transform:uppercase;cursor:pointer;padding:0;
}
.unlock-close:hover{color:var(--text);}
@media (max-width:560px){
  .unlock{gap:11px;padding:13px 14px;}
  .unlock-icon{width:30px;height:30px;}
}

/* A quiet caption beside a panel heading — the calendar's "All times Pacific", and the course
   count on the Training catalogue. Shared for the same reason the callout above it is. */
.tz-note{
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-faint);
}

/* ---- course catalog ----
   The Training tab on /student and the Owner's management page at /settings/courses render the
   same grid from the same data, so the cards live here rather than twice inline. The only
   difference between the two is which controls are drawn inside them, which is a matter for
   each page's own JavaScript. */
.course-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px;}
.course-card{
  display:flex;flex-direction:column;text-align:left;padding:0;overflow:hidden;cursor:pointer;
  background:var(--surface);border:1px solid var(--border);border-radius:12px;color:var(--text);
  font-family:'Work Sans',sans-serif;
}
.course-card:hover{border-color:var(--accent-dim);}
.course-card:hover .cover{filter:brightness(1.08);}

/* A course the student has not reached yet — the unlock sequence has not opened it, or the
   membership does not cover it, or both. Visibly not theirs, in the same register `.mod.locked`
   gives a module and `.ev.locked` gives a session: the cover desaturated, the title dimmed, the
   card flat against the page.

   And genuinely not openable. /student renders a shut course as a <div> rather than a <button>,
   with no data-id and so no click handler, so there is nothing to press — the line naming what
   opens the course is on the card itself, which is why nothing needs to be opened to read it.
   `pointer-events` is left alone deliberately: the explanation is the one thing on a shut card
   worth being able to select and re-read. The `cursor` and the flattened hover are what say so
   to a mouse, and they are also what covers a page that still draws the card as a button.

   Nothing behind a shut card was sent: every module in a course like this came back locked from
   the same request, with no video address and no PDF address on any of them. */
.course-card.locked{border-color:var(--border-soft);background:var(--bg-2);cursor:default;}
.course-card.locked .cover{filter:grayscale(1) brightness(.6);}
.course-card.locked .course-title{color:var(--text-dim);}
.course-card.locked .course-desc{color:var(--text-faint);}
.course-card.locked:hover{border-color:var(--border-soft);}
.course-card.locked:hover .cover{filter:grayscale(1) brightness(.6);}
/* Belt and braces for a shut card that somehow still holds a control: /settings/courses draws
   its own buttons into these cards, and a locked card there must not offer one either. */
.course-card.locked .btn,
.course-card.locked .mod-links{opacity:.45;pointer-events:none;}
.course-lock{
  font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--warn);opacity:.85;margin-top:9px;line-height:1.5;
}

/* The cover. cover_image_url is null on every course today, so the placeholder is the state
   that actually ships: a dark wash in the app's own reds with the film grain over it and the
   course's initials set in the display face. Deliberately not an <img> with no src, which is
   what puts a broken-image icon on the page. */
.cover{
  position:relative;aspect-ratio:16/9;overflow:hidden;transition:filter .15s ease;
  background:
    radial-gradient(120% 90% at 20% 0%, rgba(224,24,47,.28), rgba(224,24,47,0) 62%),
    linear-gradient(160deg, var(--surface-2), #0e0c0c 78%);
  display:flex;align-items:center;justify-content:center;
}
.cover img{width:100%;height:100%;object-fit:cover;display:block;}
.cover::after{
  content:"";position:absolute;inset:0;background-image:var(--grain);
  mix-blend-mode:overlay;opacity:.28;pointer-events:none;
}
.cover .cover-mark{
  font-family:'Anton',sans-serif;font-size:30px;letter-spacing:.06em;
  color:rgba(243,239,233,.16);text-transform:uppercase;user-select:none;
}
.cover .cover-tag{
  position:absolute;left:12px;bottom:10px;font-family:'IBM Plex Mono',monospace;
  font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--text-faint);
}

.course-body{padding:14px 15px 16px;display:flex;flex-direction:column;gap:8px;flex:1;}
.course-title{font-family:'Anton',sans-serif;font-size:16px;line-height:1.2;letter-spacing:.01em;}
.course-desc{
  color:var(--text-dim);font-size:12.5px;line-height:1.5;margin:0;flex:1;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
}

/* ---- progress bar ----
   A course with no modules yet reads 0% against an empty track — the bar is driven by a width
   the page only ever computes from a non-zero total. */
.prog{display:flex;flex-direction:column;gap:6px;}
.prog-track{
  height:6px;border-radius:99px;background:var(--surface-2);
  border:1px solid var(--border-soft);overflow:hidden;
}
.prog-fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--accent-dim),var(--accent-bright));}
.prog-meta{
  display:flex;justify-content:space-between;gap:8px;font-family:'IBM Plex Mono',monospace;
  font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-faint);
}
.prog-meta .done{color:var(--good);}

/* The "+ New course" tile, which sits in the grid as the last card on the Owner's page. */
.tile-new{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
  min-height:200px;border:1px dashed var(--border);border-radius:12px;background:rgba(24,22,22,.45);
  color:var(--text-dim);font-family:'Work Sans',sans-serif;cursor:pointer;padding:20px;
}
.tile-new:hover{border-color:var(--accent-dim);color:var(--text);background:var(--accent-wash);}
.tile-new .tn-plus{font-family:'Anton',sans-serif;font-size:26px;color:var(--accent-bright);line-height:1;}
.tile-new .tn-label{font-family:'Anton',sans-serif;font-size:14px;}
.tile-new .tn-hint{font-size:11.5px;color:var(--text-faint);text-align:center;line-height:1.45;}

/* ---- module list ----
   The outline of one course: optional section headings, each over a run of module rows. */
.mod-outline{display:grid;gap:9px;}
.mod-sec{display:flex;align-items:baseline;gap:10px;margin:9px 0 0;}
.mod-outline > .mod-sec:first-child{margin-top:0;}
.mod-sec .ms-t{font-family:'Anton',sans-serif;font-weight:400;font-size:14px;letter-spacing:.02em;}
.mod-sec .ms-n{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);white-space:nowrap;
}
.mod-sec .ms-rule{flex:1;border-top:1px solid var(--border-soft);}
.mod-list{display:grid;gap:9px;}
.mod{
  display:flex;gap:13px;align-items:flex-start;background:var(--surface);border:1px solid var(--border);
  border-radius:11px;padding:13px 15px;
}
.mod.done{border-color:var(--accent-dim);background:linear-gradient(180deg,var(--accent-wash),var(--surface) 72%);}
.mod-n{
  font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--text-faint);
  padding-top:2px;min-width:22px;
}
.mod-main{flex:1;min-width:0;}
.mod-t{font-size:13.5px;font-weight:600;line-height:1.35;}
.mod-sub{font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--text-faint);margin-top:4px;}
.mod-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}
.mod-actions .btn{margin-top:0;}

/* The self-mark control. A real checkbox, tinted with the accent, so it keeps its keyboard
   behaviour and its label association. */
.tick{display:flex;align-items:center;gap:8px;cursor:pointer;white-space:nowrap;}
.tick input{width:17px;height:17px;accent-color:var(--accent);cursor:pointer;margin:0;}
.tick span{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--text-faint);
}
.mod.done .tick span{color:var(--good);}
.tick input:disabled,.tick input:disabled + span{opacity:.5;cursor:progress;}

/* A module the student has not reached yet. Greyed in the same register as a locked session on
   the calendar: still legible, still numbered, visibly not theirs yet — a curriculum with holes
   in it teaches nobody what is coming.

   Non-interactive with it. The row carries no link because the response it was drawn from
   carried none, the tick is disabled, and the two rules below make sure a control that does
   reach a locked row cannot be pressed — so the row is inert whether or not the markup around
   it ever changes. The label and the unlock sentence stay selectable. */
.mod.locked{background:var(--bg-2);border-color:var(--border-soft);}
.mod.locked .mod-t{font-weight:500;color:var(--text-dim);}
.mod.locked .mod-n{color:var(--text-faint);}
.mod.locked .tick{cursor:default;}
.mod.locked .tick input:disabled{cursor:not-allowed;}
.mod.locked .tick input:disabled + span{cursor:default;}
.mod.locked .mod-links,
.mod.locked .btn,
.mod.locked .mod-pdf{opacity:.45;pointer-events:none;}
.mod-lock{
  font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--warn);opacity:.85;
}

/* How far through the video a student is, where the "mark complete" checkbox used to be.

   The checkbox is gone for students because it no longer decided anything: completion is now
   reaching the end of the video, so a control offering to do it by hand could only lie. What
   replaces it is not a control at all — it reports. The bar is the same number the ledger holds
   (netlify/lib/watch-progress.mts), so a student can see that the twenty minutes they watched
   last night were counted, which is the question a disabled checkbox would have left them asking.

   Sized and lettered to sit in the same place in the row as the tick it replaces, so a course
   list reads the same down its right-hand edge whether it is drawn for a student or for a member
   of the internal team, who keep the checkbox because they are not working through the
   curriculum. */
.mod-watch{display:flex;align-items:center;gap:8px;white-space:nowrap;}
.mod-watch .mw-bar{
  display:block;width:52px;height:4px;border-radius:3px;overflow:hidden;
  background:var(--surface-2);
  box-shadow:inset 0 0 0 1px var(--border-soft);
}
.mod-watch .mw-bar i{
  display:block;height:100%;width:0;border-radius:3px;
  background:linear-gradient(90deg,var(--accent),var(--accent-bright));
  transition:width .3s ease;
}
.mod-watch .mw-t{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--text-faint);
}
.mod-watch.is-done .mw-t{color:var(--good);}
.mod-watch.is-done .mw-bar i{background:var(--good);}
/* Nothing watched yet, so nothing to draw a bar for: an empty track next to "not started" is a
   progress bar reporting the absence of progress, which is what the words already say. */
.mod-watch.is-new .mw-bar{display:none;}
@media (prefers-reduced-motion:reduce){
  .mod-watch .mw-bar i{transition:none;}
}

/* What an unlocked module offers: the video link, and the PDF beside it when there is one. */
.mod-links{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px;}
.mod-links .btn{margin-top:0;}
.mod-pdf{
  display:inline-flex;align-items:center;gap:6px;font-family:'IBM Plex Mono',monospace;
  font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-dim);
  text-decoration:none;border:1px solid var(--border);border-radius:7px;padding:5px 9px;
  background:var(--surface-2);
}
.mod-pdf:hover{border-color:var(--accent-dim);color:var(--text);}

/* ---- a module's video, played here ----
   The Mux player, for a module whose file has been uploaded to Mux. It replaces the "Watch the
   video" link on that module only — every module still on its original video link keeps the link
   — so during the migration a course list carries both, which is deliberate and is what the
   Mux / Video link indicator below labels.

   Both get the same frame. A course part-way through the migration should read as one set of
   videos on two backends rather than as two kinds of thing, so .mod-video-frame wraps the player
   and the not-yet-moved module's link panel alike, and /assets/course-video.js emits it for both.

   ---- the frame, in three layers -------------------------------------------

   This is the most-looked-at object in the app, so it is framed rather than outlined, and the
   frame is built outward from the glass:

     the gold ring    A metallic gold bezel laid on the very edge of the picture. It is the edge
                      the eye lands on first and the only warm colour in the treatment. Muted
                      old gold, deliberately not the brighter gold /internal's board and the
                      trackers tab use for a prize: this is a frame somebody looks past all day,
                      and a bright yellow rectangle would compete with the video inside it.
     the black edge   A near-black hairline immediately outside the gold, so the gold meets
                      shadow and not red. Without it the red bleeds into the metal and the gold
                      turns orange, which is the one way a treatment like this reads as cheap.
     the red glow     The brand red, behind the frame as light rather than on it as a border.
                      Stacked shadows: brightest hugging the gold, weighted downward, widening
                      and cooling into the palette's black as they travel. A hard red rectangle
                      around an empty 16/9 reads as a placeholder waiting to be filled; light
                      coming off the edge reads as a screen that is switched on, and light
                      pooling underneath is what lifts the frame off the row it sits in.

   So the depth is consistent rather than decorative: every offset is downward, the near shadows
   are tight and red, the far ones are wide and black. That is what an object standing off a dark
   page actually does to the surface under it, and it is why the gold reads as sitting forward of
   the row instead of being printed on it.

   The red is the value already on the sign-in screen and the PWA icon, and the player's own
   controls are tinted to match through its accent-color / primary-color / secondary-color
   attributes, so the tint runs through the controls instead of stopping at the edge.

   16/9 is set on <mux-player> itself rather than here, because the player wants an aspect ratio
   and not a height. */
.mod-video{margin:12px 0 3px;}
.mod-video-save{margin-top:10px;font-size:13px;line-height:1.5;color:var(--muted);}
.mod-video-save button{margin-left:6px;}
.mod-video-frame{
  /* Scoped to the component and named for it rather than added to :root as --gold, because
     /internal's board panel and the trackers tab each define a --gold of their own — a brighter,
     prize gold — and a player drawn inside either of those subtrees must not inherit it. */
  --vf-gold:#b08f4f; --vf-gold-lit:#e6cf9a; --vf-gold-deep:#6b5225;

  position:relative;border-radius:11px;overflow:hidden;background:var(--bg-deep);
  /* Reading outward: the black edge, the red hugging the gold, the red pooling below it, then
     the black bloom the whole frame sits in. The negative spreads are what keep the sideways
     reach inside the module row's own padding, so the light travels down — where an object's
     shadow goes — rather than out across the column. */
  box-shadow:
    0 0 0 1px rgb(var(--bg-deep-rgb) / .92),
    0 0 9px -1px rgb(var(--accent-bright-rgb) / .26),
    0 1px 14px -3px rgb(var(--accent-rgb) / .58),
    0 7px 22px -8px rgb(var(--accent-rgb) / .5),
    0 14px 34px -12px rgb(var(--accent-rgb) / .3),
    0 20px 44px -16px rgb(var(--bg-deep-rgb) / .95),
    0 30px 66px -22px rgb(var(--bg-deep-rgb) / .9);
}

/* The gold ring, drawn over the outermost 2px of the picture rather than set as a border on the
   frame. Two reasons it is an overlay:

     * A border takes one flat colour, and flat gold is paint. Metal is a highlight and a shadow
       on the same edge, which needs a gradient — light along the top left, dark along the bottom
       right, a second catch of light three quarters round. The gradient is laid across the whole
       frame so each side of the ring picks up the part of it facing that way, which is what lets
       2px read as a bevel instead of as a yellow line.
     * It has to sit on top. .mod-video-offsite below paints its own background on this same
       element, so a border here would be painted over on the link panel; an overlay is the one
       construction that comes out identical on the player and on the panel, which is the
       consistency the shared frame exists for in the first place.

   The two mask layers are what turn a filled rectangle into a ring: one clipped to the content
   box, one to the whole box, composited so the inner is subtracted from the outer. Both clips
   follow border-radius — a content box inside an 11px corner is itself rounded, to 9px — so the
   ring holds an even thickness around the corners rather than pinching at them.

   Above the scrim and the mark in z-order, and `pointer-events:none`, so the 2px it lays over
   the bottom of the control bar can never take a click meant for the player underneath. */
.mod-video-frame::after{
  content:"";position:absolute;inset:0;z-index:4;pointer-events:none;
  border-radius:inherit;padding:2px;
  background:linear-gradient(155deg,
    var(--vf-gold-lit) 0%, var(--vf-gold) 22%, var(--vf-gold-deep) 46%,
    var(--vf-gold) 62%, var(--vf-gold-lit) 76%, var(--vf-gold-deep) 100%);
  -webkit-mask:linear-gradient(#000,#000) content-box, linear-gradient(#000,#000);
  mask:linear-gradient(#000,#000) content-box, linear-gradient(#000,#000);
  -webkit-mask-composite:xor;
  mask-composite:exclude;
}
/* Where the two layers cannot be composited the subtraction never happens and the gradient
   would cover the whole video, so an engine without it gets flat gold instead. The same 2px in
   the same place, drawn inward from inset:0, so nothing moves — only the bevel is lost. */
@supports not ((mask-composite:exclude) or (-webkit-mask-composite:xor)){
  .mod-video-frame::after{
    padding:0;background:none;border:2px solid var(--vf-gold);
    -webkit-mask:none;mask:none;
  }
}
.mod-video-frame mux-player{display:block;width:100%;background:var(--bg-deep);}
/* Before /assets/course-video.js's CDN script has upgraded the element it is an unknown tag with
   no layout of its own. Given the same box, the frame appears with the row rather than snapping
   in a moment later. */
.mod-video-frame mux-player:not(:defined){aspect-ratio:16/9;}

/* The control bar's buttons, given a thumb's worth of room on a touch screen.

   Measured rather than guessed: on a 390px phone the player's own fullscreen button is 30x26
   CSS pixels and sits 6px off the bottom-right corner of the frame. Both platform guidelines
   ask for 44, and the gap matters more than it sounds — a tap scattered realistically around
   that icon found it about a quarter of the time, and almost every miss landed on nothing at
   all, which is the student's report exactly: press the corner six or seven times and the
   video eventually goes full screen. The same taps find a 44px target most of the time.

   Reached through ::part, which media-theme forwards: the button carries part="bottom
   fullscreen button" and the theme exports `bottom` and `button`, so this is the player's own
   styling surface rather than a selector aimed into its shadow DOM. `fullscreen` is not among
   the exported names, so every button on the bottom bar grows — mute and play want the same
   44px for the same reason, and the bar was measured at 320, 360 and 390px to confirm the
   scrubber still has its room and nothing overflows.

   Coarse pointers only. A mouse hits 30x26 first time every time, and a desktop player that
   suddenly wore phone-sized chrome would be a regression for a problem it does not have. */
@media (hover:none) and (pointer:coarse){
  .mod-video-frame mux-player::part(bottom button){min-width:44px;height:44px;}
}
.mod-video .mod-links{margin-top:9px;}

/* The mark. Top left, because <mux-player> puts its cast and AirPlay buttons top right and we
   never set the visible `title` attribute that would otherwise occupy this corner.

   It plays small — a module row inside the training tab, not a cinema — so the width is clamped
   against the frame and held at just over half opacity, with the scrim below and a drop shadow
   doing the separating instead of a plate or a box. At the sizes this renders it should read as
   a mark on the glass and not as a second piece of UI competing with the play button.

   It goes while the video is playing, on the principle that branding belongs on an idle player
   and nothing belongs on top of a video someone is watching. `pointer-events:none` throughout,
   so it can never eat a click meant for the player underneath. */
.mod-video-mark{
  position:absolute;top:11px;left:12px;z-index:3;display:block;pointer-events:none;
  width:clamp(44px, 10.5%, 74px);opacity:.55;
  transition:opacity .28s ease;
}
/* Flattened to a white silhouette rather than left as the file's brushed chrome. The chrome is
   a gradient across 1744px and there is nothing left of it at 60 — downscaled it turns into grey
   mush, and grey mush over a bright poster frame reads as a mark on the lens rather than a mark
   on the video. The silhouette is the identity anyway: the crossed blades are the shape people
   recognise, and flat white holds its edge over a dark room and a white slide alike. The red
   stays where it belongs, on the frame and in the controls. */
.mod-video-mark img{
  display:block;width:100%;height:auto;
  filter:brightness(0) invert(1) drop-shadow(0 1px 5px rgb(var(--bg-deep-rgb) / .9));
}

/* A scrim under it. A poster frame is as likely to be a bright slide as a dark room, and half a
   mark disappearing into a white background looks like a rendering fault rather than restraint.
   So the top edge is darkened first — full width, a soft linear falloff, no edge of its own —
   and the mark sits on that. Along the top rather than in the corner deliberately: a blob in one
   corner reads as a smudge, whereas a darker top edge is what a video player looks like, and is
   the same gradient mux-player draws under its own controls. Both go on play, so nothing sits
   over a video someone is watching. */
.mod-video-frame::before{
  content:"";position:absolute;top:0;left:0;z-index:2;pointer-events:none;
  width:100%;height:26%;max-height:120px;
  background:linear-gradient(to bottom, rgb(var(--bg-deep-rgb) / .62), rgb(var(--bg-deep-rgb) / 0));
  transition:opacity .28s ease;
}
.mod-video-frame.is-playing::before,
.mod-video-frame.is-playing .mod-video-mark{opacity:0;}
/* Playing or not, it is decoration — anyone who has asked for less movement keeps whichever
   state they are in rather than being faded between them. */
@media (prefers-reduced-motion:reduce){
  .mod-video-frame::before,
  .mod-video-mark{transition:none;}
}

/* "You cannot skip ahead", said at the moment a student finds out — the seek they just dragged
   forward having been put straight back by /assets/course-video.js. Only drawn on a player that
   is actually enforcing, and only shown for a few seconds after a blocked seek: a permanent
   caption on every video would be a rule announced to everyone who was never going to break it.

   Top centre, clear of the mark in the left corner and of the player's own cast and AirPlay
   buttons on the right, and above the control bar rather than over it. It carries its own dark
   plate because the frame's scrim has faded out by the time this appears — a video that is
   playing is exactly when this fires. `pointer-events:none` throughout: it must never take a
   click meant for the player, least of all from somebody trying to seek again. */
.mod-video-hold{
  position:absolute;top:11px;left:50%;transform:translateX(-50%);z-index:5;pointer-events:none;
  max-width:74%;text-align:center;
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--text);
  border:1px solid rgb(var(--accent-rgb) / .5);border-radius:7px;
  padding:6px 11px;
  background:rgb(var(--bg-deep-rgb) / .88);
  box-shadow:0 2px 12px -4px rgb(var(--bg-deep-rgb) / .95);
  opacity:0;transition:opacity .2s ease;
}
.mod-video-hold.is-shown{opacity:1;}
@media (prefers-reduced-motion:reduce){
  .mod-video-hold{transition:none;}
}

/* A module still on its original video link. Same frame, so the list stays even, but the video
   is not ours to embed yet — an <iframe> here would put the third-party player's own branding
   back inside our frame, which is the thing the migration is removing. A link on the brand field
   instead, holding the module's place until its file is uploaded. */
.mod-video-offsite{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:11px;
  aspect-ratio:16/9;padding:18px;text-align:center;
  background:
    radial-gradient(120% 110% at 50% 0%, rgb(var(--accent-rgb) / .12), rgb(var(--accent-rgb) / 0) 66%),
    var(--bg-deep);
}
/* Here the mark is the poster rather than a corner bug: there is no picture to sit on the corner
   of, and a crest holding the middle of an empty 16/9 is what a video looks like before it
   plays. 16/9 for the same reason — a course part-way through the migration should have rows of
   one height, not a row of players and a row of shorter panels. No scrim, because the field
   underneath is already ours. */
.mod-video-offsite::before{display:none;}
.mod-video-offsite .mod-video-mark{
  position:static;z-index:auto;width:clamp(50px, 15%, 86px);opacity:.34;
}
.mvo-play{
  font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text);text-decoration:none;border:1px solid rgb(var(--accent-rgb) / .45);
  border-radius:8px;padding:9px 15px;background:rgb(var(--bg-deep-rgb) / .6);
  transition:border-color .18s ease, background .18s ease;
}
.mvo-play:hover{border-color:var(--accent-bright);background:var(--accent-wash);}
.mvo-note{
  font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.09em;text-transform:uppercase;
  color:var(--text-faint);
}

/* Which of the two hosts is live for a module. Drawn on the Admin's curriculum page, where the
   whole point is to be able to read a course's migration state off the list at a glance: red for
   ours, neutral for the link that has not been moved yet, faint for a module with no video. */
.vid-tag{
  display:inline-flex;align-items:center;font-family:'IBM Plex Mono',monospace;font-size:9px;
  letter-spacing:.11em;text-transform:uppercase;border-radius:6px;padding:3px 7px;
  border:1px solid var(--border);color:var(--text-faint);background:var(--surface-2);
  white-space:nowrap;cursor:help;
}
.vid-mux{border-color:var(--accent-dim);color:var(--accent-bright);background:var(--accent-wash);}
.vid-link{color:var(--text-dim);}
.vid-none{opacity:.7;}

/* The upload's own line: where it has got to, and why it stopped if it did. */
.vid-status{
  display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:6px;
  font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--text-dim);line-height:1.5;
}
.vid-status.vid-bad{color:var(--warn);}

/* A pulsing dot rather than a rotating ring. Transcoding reports no percentage — Mux does not
   give one — so this says "still going" and claims nothing about how far along it is, which is
   the honest signal for a wait of unknown length. */
.vid-spin{
  width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none;
  animation:vid-pulse 1.1s ease-in-out infinite;
}
@keyframes vid-pulse{0%,100%{opacity:.25;}50%{opacity:1;}}

/* The upload half, which does have a real percentage: XHR reports the bytes going out. */
.vid-bar{
  flex:1;min-width:70px;height:3px;border-radius:2px;background:var(--surface-2);
  overflow:hidden;
}
.vid-bar-fill{display:block;height:100%;background:var(--accent);transition:width .2s linear;}

/* ---- person avatars ----
   profiles.avatar_url is a link the account pastes in itself, and it is null on almost every
   row — so, exactly as with the course covers above, the placeholder is the state that
   actually ships rather than a fallback for a broken case: the same dark accent wash, the same
   film grain, the person's initials set in the display face. Deliberately not an <img> with no
   src, which is what puts a broken-image icon next to somebody's name.

   The initials are set far brighter than .cover-mark's. A cover mark is a texture behind a
   title that is already on screen; an avatar's initials ARE the identification, and at 32px
   they have to be legible rather than atmospheric.

   Used by /internal, /student and /roster. Three sizes and no more: sm for a table row, the
   default for a heading, lg for the account control that sets it. */
.avatar{
  position:relative;flex:0 0 auto;width:44px;height:44px;border-radius:50%;overflow:hidden;
  display:flex;align-items:center;justify-content:center;border:1px solid var(--border);
  background:
    radial-gradient(120% 90% at 25% 0%, rgba(224,24,47,.30), rgba(224,24,47,0) 64%),
    linear-gradient(160deg, var(--surface-2), #0e0c0c 78%);
}
/* image-orientation is the browser's own default for an <img>, and it is stated here anyway.
   A photo off a phone carries its rotation as an EXIF tag rather than in the pixel order, this
   property is what honours it, and it is an inherited one -- so a single `image-orientation:none`
   on any ancestor, anywhere, would silently lay every face on the site on its side. Saying it
   on the image itself means that cannot happen from above. */
.avatar img{width:100%;height:100%;object-fit:cover;display:block;image-orientation:from-image;}
.avatar::after{
  content:"";position:absolute;inset:0;background-image:var(--grain);
  mix-blend-mode:overlay;opacity:.26;pointer-events:none;
}
.avatar .av-mark{
  font-family:'Anton',sans-serif;font-size:15px;letter-spacing:.05em;line-height:1;
  color:rgba(243,239,233,.62);text-transform:uppercase;user-select:none;
}
.avatar.avatar-sm{width:32px;height:32px;}
.avatar.avatar-sm .av-mark{font-size:11.5px;}
.avatar.avatar-lg{width:66px;height:66px;}
.avatar.avatar-lg .av-mark{font-size:23px;}

/* ---- the Members directory, the member profile and direct messages ----
   Rendered by /assets/members.js, which is mounted as a tab on both signed-in dashboards
   (/internal and /student). Shared here rather than kept inline on either page for the reason
   this stylesheet exists at all: it is one feature appearing in two places, and two copies of
   its layout is two places for the two dashboards to drift apart.

   Built on the primitives already above — .avatar, .card, .prog, .pill, .msg, .btn — so a
   member card and a course card are recognisably the same product. Only the arrangement is
   new. */

.mem-grid{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;
}

/* ---- the directory's columns ----

   The roster is drawn in labelled columns — Staff, Free accounts, Intro students, Academy
   students — rather than as one undivided grid of everybody. Which columns there are is
   /api/members' answer and not this stylesheet's: a reader who may not see membership tiers is
   sent two instead of four, and .mem-cols carries the count it was handed as a class.

   Explicit tracks rather than the auto-fill the card grids above use, because the count is the
   point here: auto-fill on a wide panel would happily make a fifth column out of nothing. */
.mem-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 18px;align-items:start;}
/* Two columns, on the copy of the tab that was sent two: the reader who may not see membership
   tiers gets Staff and Members, and more tracks would leave most of the panel permanently blank
   beside them. */
.mem-cols-2{grid-template-columns:repeat(2,minmax(0,1fr));}
/* Four, on the staff copy. The panel is .wrap, so 1032px at its widest — four tracks and the
   18px gaps put each column at ~244px, which a card whose fixed chrome is a 44px photo and 30px
   of padding still reads at. The folding is in the two queries at the end of this block. */
.mem-cols-4{grid-template-columns:repeat(4,minmax(0,1fr));}
.mem-col{min-width:0;}
.mem-col-head{
  display:flex;align-items:baseline;gap:10px;margin-bottom:11px;padding-bottom:8px;
  border-bottom:1px solid var(--border);
}
/* The same treatment as the "17 on your team" tag this sits under, one step louder: it is a
   heading, and .tag is a caption. */
.mem-col-title{
  margin:0;flex:1 1 auto;min-width:0;
  font-family:'IBM Plex Mono',monospace;font-weight:600;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--text-dim);
}
.mem-col-count{
  flex:0 0 auto;font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--text-faint);
}
/* One card per row inside a column: the cards are already as narrow as they read well at, and a
   column that reflowed them two-up would undo the grouping it is there to show. */
.mem-col-grid{grid-template-columns:minmax(0,1fr);}
.mem-col-empty{
  margin:0;font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);
}
@media (max-width:900px){ .mem-cols{grid-template-columns:minmax(0,1fr);gap:24px;} }
/* The four-column copy folds in half before it stacks, rather than going from four tracks
   straight to one — the same "halve, then stack" step the two- and three-column copies make in
   one move, with the extra step four columns need to get there.

   1080px is where .wrap stops being full width and every track starts losing pixels together,
   so it is the point at which four columns first cost something. 720px is where two tracks stop
   fitting a photo, a name and a role label side by side. Both of these come AFTER the 900px rule
   above deliberately: they carry the same specificity, so source order is what lets the staff
   copy hold two columns through the band where the others have already stacked. */
@media (max-width:1080px){ .mem-cols-4{grid-template-columns:repeat(2,minmax(0,1fr));} }
@media (max-width:720px){ .mem-cols-4{grid-template-columns:minmax(0,1fr);} }
.mem-card{
  display:flex;align-items:center;gap:12px;text-align:left;width:100%;
  background:var(--surface);border:1px solid var(--border);border-radius:12px;
  padding:14px 15px;cursor:pointer;color:inherit;font-family:'Work Sans',sans-serif;
}
.mem-card:hover{border-color:var(--accent-dim);background:var(--bg-2);}
.mem-card-main{min-width:0;flex:1 1 auto;}
/* display:block because these are spans — a member card is a <button>, so everything inside it
   is phrasing content, and text-overflow does nothing to an inline box. */
.mem-card-name{
  display:block;font-weight:600;font-size:14px;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.mem-card-sub{
  display:block;font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);margin-top:3px;
}
/* Unread direct messages from this person. Deliberately loud — it is the only thing on the
   directory that is asking for something. */
.mem-badge{
  flex:0 0 auto;min-width:20px;height:20px;border-radius:99px;padding:0 6px;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--accent);color:#fff;
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;font-weight:600;
}

/* ---- one member's profile ---- */
.mem-head{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:18px;}
.mem-head-main{min-width:0;flex:1 1 220px;}
.mem-head-name{font-family:'Anton',sans-serif;font-weight:400;font-size:24px;line-height:1.1;}
.mem-head-meta{
  display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;margin-top:7px;
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);
}
/* The contact line under the meta row. Not uppercased and not letter-spaced, unlike the chips
   above it, because it is an address somebody reads back rather than a label. */
.mem-head-mail{
  margin-top:6px;font-family:'IBM Plex Mono',monospace;font-size:11.5px;
  overflow-wrap:anywhere;
}
.mem-head-mail a{color:var(--text-dim);text-decoration:none;border-bottom:1px solid var(--border);}
.mem-head-mail a:hover{color:var(--text);}
.mem-head-acts{display:flex;gap:8px;flex-wrap:wrap;}

.mem-section{margin-top:22px;}
.mem-section > h3{
  font-family:'Anton',sans-serif;font-weight:400;font-size:17px;margin:0 0 4px;
}
.mem-section > .mem-note{
  color:var(--text-dim);font-size:13px;line-height:1.5;margin:0 0 12px;
}

/* A course line in the training section: title, then the shared progress bar. */
.mem-course{padding:12px 0;border-top:1px solid var(--border-soft);}
.mem-course:first-child{border-top:none;}
.mem-course-top{
  display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:7px;
}
.mem-course-title{font-weight:600;font-size:13.5px;}
.mem-course-pct{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--text-faint);}
.mem-course.is-locked .mem-course-title{color:var(--text-faint);}

/* One graded round. The score leads, because on a page of them the score is what is being
   scanned; strengths and improvements sit under it as two short lists. */
.mem-round{
  background:var(--surface);border:1px solid var(--border);border-radius:10px;
  padding:13px 14px;
}
.mem-round + .mem-round{margin-top:9px;}
.mem-round-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px;}
.mem-score{
  font-family:'Anton',sans-serif;font-size:20px;line-height:1;min-width:44px;
}
.mem-score.is-pass{color:var(--good);}
.mem-score.is-mid{color:var(--warn);}
.mem-score.is-low{color:var(--accent-bright);}
.mem-round-who{font-weight:600;font-size:13.5px;}
.mem-round-when{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--text-faint);margin-left:auto;
}
.mem-lists{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px 18px;}
.mem-list-head{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);margin-bottom:5px;
}
.mem-list{margin:0;padding-left:17px;color:var(--text-dim);font-size:12.5px;line-height:1.5;}
.mem-notes{
  margin:10px 0 0;color:var(--text-dim);font-size:12.5px;line-height:1.55;
  white-space:pre-wrap;overflow-wrap:break-word;
}

/* The contract band. Coach / admin / owner only — see netlify/lib/members.mts, which is what
   decides that; a caller without it gets no contract in the response at all, so there is
   nothing here for a stylesheet to hide. */
.mem-contract-row{display:flex;flex-wrap:wrap;gap:10px 22px;align-items:center;}
.mem-contract-field{min-width:0;}
.mem-contract-label{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);margin-bottom:4px;
}
.mem-contract-value{font-size:13.5px;color:var(--text);overflow-wrap:anywhere;}
.mem-contract-value a{color:var(--accent-bright);}
.mem-contract-form{margin-top:14px;}
.mem-contract-form .field-label{margin-top:10px;}
.mem-contract-form input[type=date]{
  width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);
  font-family:'Work Sans',sans-serif;font-size:14.5px;padding:12px 13px;border-radius:9px;
}
.mem-form-acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px;}
/* The one line that rides with a contract wherever it is shown or set. Server copy --
   CONTRACT_PII_NOTICE in netlify/lib/members.mts -- so the sentence is the same on the
   member profile, on the Coach tools tab and on the profile card, and amber because it
   is a "read this before you act" rather than an error. */
.mem-warn{
  margin:12px 0 0;color:var(--warn);opacity:.9;font-size:12.5px;line-height:1.5;
}

/* ---- completed agreements on a profile ----
   The same rows the Agreements tab lists, on the person they are with, gated
   with the contract band above them. Read-only here and locked everywhere: a
   completed agreement cannot be changed on either side, so there is no control
   in this band and nothing for one to do.

   The accent left edge is the same signal .docs-done carries on the Agreements
   tab -- "this one is closed" -- so the two surfaces say it the same way. */
.mem-agrs{display:flex;flex-direction:column;gap:10px;}
.mem-agr{
  background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent);
  border-radius:10px;padding:13px 15px;
}
.mem-agr-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;}
.mem-agr-top strong{font-size:14px;color:var(--text);}
.mem-agr-when{
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--accent-bright);
}
.mem-agr-by{margin:4px 0 0;color:var(--text-dim);font-size:12px;line-height:1.5;}
/* The negotiated terms as they were typed on the call, newlines kept -- a rate
   and a schedule on one line each is how they were written and how they should
   read back. */
.mem-agr-terms{
  margin:9px 0 0;color:var(--text);font-size:13px;line-height:1.55;
  white-space:pre-wrap;overflow-wrap:break-word;
}
/* Addresses and the masked card. The card line is the brand, the last four and
   the expiry -- this page is never sent a full number. */
.mem-agr-facts{
  display:grid;grid-template-columns:max-content 1fr;gap:4px 14px;margin:10px 0 0;
  font-size:12.5px;line-height:1.5;
}
.mem-agr-facts dt{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--text-faint);padding-top:3px;
}
.mem-agr-facts dd{margin:0;color:var(--text-dim);white-space:pre-wrap;overflow-wrap:anywhere;}
@media (max-width:560px){
  .mem-agr-facts{grid-template-columns:1fr;gap:2px;}
  .mem-agr-facts dd{margin-bottom:6px;}
}

/* ---- the direct-message thread ---- */
.dm-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px;}
.dm-head-main{min-width:0;flex:1 1 auto;}
.dm-thread{
  display:flex;flex-direction:column;gap:9px;
  max-height:460px;overflow-y:auto;padding:4px 2px;margin-bottom:14px;
}
.dm-msg{max-width:min(78%,520px);align-self:flex-start;}
.dm-msg.is-mine{align-self:flex-end;}
.dm-bubble{
  background:var(--surface);border:1px solid var(--border);border-radius:12px;
  padding:10px 13px;font-size:13.5px;line-height:1.5;color:var(--text);
  white-space:pre-wrap;overflow-wrap:break-word;
}
.dm-msg.is-mine .dm-bubble{
  background:var(--accent-wash);border-color:var(--accent-dim);
}
.dm-when{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--text-faint);margin-top:4px;
}
.dm-msg.is-mine .dm-when{text-align:right;}
.dm-compose{display:flex;flex-direction:column;gap:10px;}
.dm-compose textarea{min-height:74px;}
.dm-compose-acts{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}

/* ---- the persona ring ----
   A coloured glow around a person's photo saying which opponent they are currently on in the
   Sparring Room. Read left to right on a roster it is the fastest possible answer to "how far
   along is this person", which is why it is on the photo rather than in a column.

   The colour is keyed to personas.difficulty_order, NOT to a persona's name: an Owner reorders
   the ladder at /settings/personas, and the ring follows that ordering rather than a list of
   names baked in here. Level 1 green through level 4 red is the shipped ladder — Jordan,
   Marcus, Casey, Karen — and a fifth opponent added tomorrow inherits the top colour rather
   than rendering ringless.

   The reds are deliberately not --accent. The brand red sits behind everything on these pages
   and a ring in it would read as chrome; #ef4444 is far enough off it to read as a status.

   Applied as box-shadow rather than as a border or an outline for three reasons: it costs no
   layout, so nothing shifts when a ring appears; box-shadow is not clipped by the
   overflow:hidden that keeps the photo circular; and the layers let a dark gap sit between the
   photo and the ring, which is what keeps a green ring legible against a light photo.

   .ring-0 is the honest "no claim" state — WARCOX's own staff, who coach against these
   opponents rather than climb past them, and any account whose ladder could not be read. Grey
   rather than absent so a ringed page does not look half-rendered.

   Both selectors are here because two shapes of avatar exist: .avatar from this stylesheet
   (/internal, /student, /roster, /assets/avatar.js) and .av, /student's own smaller board
   avatar. The class names are identical on both, so /assets/persona-ring.js does not need to
   know which page it is on.

   `ring` is this stylesheet's, and every page that shows a face loads this stylesheet. A
   feature stylesheet that loads alongside it and wants a ring of its own -- a progress dial, a
   streak indicator -- has to name it something else, prefixed the way that file prefixes
   everything else it owns. /assets/trackers.css once declared a bare `.ring` for an SVG dial
   whose whole job is to sit rotated a quarter turn, and because it loads after this file on
   /internal, every ringed avatar on that page -- photos and initials alike -- came out
   sideways. It is `.trk-ring` now. Nothing here can defend against the next one; the rule is
   the defence. */
.ring{
  --ring-color:rgba(243,239,233,.22);
  box-shadow:
    0 0 0 2px var(--bg),
    0 0 0 3.5px var(--ring-color),
    0 0 11px -1px var(--ring-color);
}
.ring-l1{--ring-color:#3ddc84;}
.ring-l2{--ring-color:#f2c14e;}
.ring-l3{--ring-color:#f28c28;}
.ring-l4{--ring-color:#ef4444;}
/* Anything past the fourth rung keeps the top colour rather than falling back to grey. */
.ring-lx{--ring-color:#ef4444;}
/* A ring the ladder could not compute. Same grey as .ring's default, named so the markup can
   say which of the two greys it means. */
.ring-0{--ring-color:rgba(243,239,233,.22);}

/* A ring needs a little air around it or it collides with whatever is next to the photo. Only
   applied to ringed avatars, so an unringed page is pixel-identical to before. */
.avatar.ring,.av.ring{margin:2px;}

/* The legend under the Members directory, and anywhere else the colours need naming. */
.ring-key{display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center;margin:0 0 16px;}
.ring-key-item{
  display:inline-flex;align-items:center;gap:7px;
  font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-faint);
}
.ring-key-dot{
  width:10px;height:10px;border-radius:50%;flex:0 0 auto;
  background:var(--ring-color,rgba(243,239,233,.22));
  box-shadow:0 0 7px -1px var(--ring-color,rgba(243,239,233,.22));
}

/* ---- the account's own photo control ----
   Rendered by /assets/avatar.js into a host on /internal and /student. Collapsed to one line
   until it is opened, because it is a thing you do once and then never look at again. */
.acct{
  display:flex;align-items:center;gap:15px;flex-wrap:wrap;
  background:var(--surface);border:1px solid var(--border);border-radius:12px;
  padding:14px 16px;margin-bottom:20px;
}
.acct-main{flex:1 1 200px;min-width:0;}
.acct-label{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.15em;
  text-transform:uppercase;color:var(--text-faint);
}
.acct-state{font-size:13px;color:var(--text-dim);margin-top:4px;line-height:1.45;}
.acct-actions{display:flex;gap:8px;flex-wrap:wrap;}
.acct-actions .btn{margin-top:0;}
.acct-form{flex:1 1 100%;}
.acct-form .field-label{margin-top:4px;}
.acct-form .acct-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start;}
.acct-form input[type=url]{flex:1 1 260px;}
.acct-form .btn{margin-top:0;}

/* Choosing a file is the primary route and sits at the top of the panel, so it gets the solid
   accent button and the paste-a-link row below it is demoted to a ghost one. The control is a
   <label> wrapping the real input: a file input cannot be restyled, but a label is a click
   target for the input it contains, so this is a normal-looking button that opens the phone's
   photo library or the desktop file browser with no script in the middle. */
.acct-pick{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:2px;}
.acct-pick-btn{display:inline-flex;align-items:center;cursor:pointer;position:relative;}
/* Clipped rather than display:none — a hidden-but-present input still takes keyboard focus, so
   the button is reachable by tab and answers to Enter and Space the way it looks like it will. */
.acct-file{
  position:absolute;width:1px;height:1px;padding:0;margin:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
.acct-pick-btn:focus-within{outline:2px solid var(--accent-bright);outline-offset:2px;}
.acct-hint{font-size:12px;color:var(--text-faint);line-height:1.45;flex:1 1 200px;}

/* The paste-a-link route, kept for anyone who already has a photo hosted. */
.acct-alt{margin-top:14px;padding-top:12px;border-top:1px solid var(--border-soft);}
.acct-or{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.15em;
  text-transform:uppercase;color:var(--text-faint);
}
.acct-alt .field-label{margin-top:10px;}
.acct-foot{margin-top:14px;}

/* While an upload is in flight. Determinate on purpose: a photo off a phone is a few megabytes
   over whatever signal is going, and a spinner that cannot say how far along it is reads as a
   page that has stopped working. */
.acct-prog{margin-top:4px;}
.acct-bar{
  height:6px;border-radius:99px;background:var(--surface-2);
  border:1px solid var(--border);overflow:hidden;
}
.acct-bar span{
  display:block;height:100%;width:0;border-radius:99px;
  background:linear-gradient(90deg,var(--accent),var(--accent-bright));
  transition:width .18s linear;
}
.acct-prog .acct-hint{display:block;margin-top:7px;flex:none;}
@media (prefers-reduced-motion:reduce){ .acct-bar span{transition:none;} }

/* ---- misc ---- */
.link{color:var(--accent-bright);text-decoration:none;}
.link:hover{text-decoration:underline;}
.rule{border:none;border-top:1px solid var(--border-soft);margin:26px 0;}
.row-between{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;}
.mono{font-family:'IBM Plex Mono',monospace;}
.dim{color:var(--text-dim);}
.faint{color:var(--text-faint);}

/* ---- the Affiliate Portal ----
   Mounted from /assets/affiliate.js into both /student and /internal, so the styles live here
   with the rest of the shared register rather than in either page.

   `.aff.locked` is the free-tier treatment, and it is the same idea as `.mod.locked` and
   `.ev.locked`: the panel is legible, visibly not theirs, and — the part that matters — the
   thing being greyed out was never in the response. There is no code, no ledger and no
   referral list behind this, because the endpoint did not send any. */
.aff-next-check{margin:18px 0 26px;}
.aff-play{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:28px;align-items:center;
  padding:24px;border:1px solid rgb(var(--gold-rgb) / .3);border-radius:16px;
  background:linear-gradient(125deg,rgb(var(--gold-rgb) / .08),transparent 58%),var(--surface-2);}
.aff-play h3{font-family:'Anton',sans-serif;font-size:clamp(28px,5vw,44px);letter-spacing:.02em;
  margin:4px 0 8px;text-transform:uppercase;}
.aff-play p{max-width:620px;margin:0;color:var(--text-dim);line-height:1.65;}
.aff-play .aff-media{min-width:190px;text-align:center;}
.aff-play .aff-media-playing{width:min(680px,58vw);}
.aff-play .aff-media-playing .mod-video-frame{overflow:hidden;border-radius:12px;}
@media(max-width:680px){
  .aff-play{grid-template-columns:1fr}
  .aff-play .aff-media{text-align:left;}
  .aff-play .aff-media-playing{width:100%;}
}
.aff-hero{
  border:1px solid var(--accent-dim);border-radius:12px;padding:17px 18px;margin-bottom:18px;
  background:linear-gradient(180deg,var(--accent-wash),var(--surface) 80%);
}
.aff-hero .pk{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--accent-bright);
}
.aff-hero h3{font-family:'Anton',sans-serif;font-weight:400;font-size:18px;margin:7px 0 6px;}
.aff-hero p{margin:0;color:var(--text-dim);font-size:13px;line-height:1.55;}

/* The link and its copy button. The address is in a readonly input rather than in a <p> so it
   can be selected and copied by hand on a browser that refuses the clipboard API. */
.aff-link{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:13px;}
.aff-link input{
  flex:1 1 260px;font-family:'IBM Plex Mono',monospace;font-size:12.5px;
  background:var(--bg-2);color:var(--text);
}
.aff-link .btn{margin-top:0;flex:0 0 auto;}
.aff-code{font-family:'IBM Plex Mono',monospace;color:var(--accent-bright);}

.aff-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin-bottom:18px;}
.aff-stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:15px 16px;}
.aff-stat .k{
  font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--text-faint);
}
.aff-stat .v{font-family:'Anton',sans-serif;font-size:24px;margin-top:5px;}
.aff-stat .v.money{color:var(--good);}

.aff-sec{margin-top:26px;}
.aff-sec h3{font-family:'Anton',sans-serif;font-weight:400;font-size:15px;margin:0 0 10px;}

/* The program video and script. Shown to every tier — the pitch is the thing a locked panel is
   advertising — and greyed with the panel when the membership does not include the program. */
.aff-media{display:flex;gap:10px;flex-wrap:wrap;}
.aff-media .btn{margin-top:0;}

/* The Owner's entry form: three picks and an amount, laid out to wrap on a phone. */
.aff-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));align-items:end;}
.aff-form .btn{margin-top:0;}
.aff-form .field-label{margin-top:0;}
.aff-split{
  font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--text-dim);
  align-self:center;padding-bottom:11px;
}

/* Free tier. Every control inside is disabled by the mount as well as dimmed here — the class
   is the appearance, the `disabled` attribute is the behaviour, and neither is the boundary. */
.aff.locked .aff-hero{border-color:var(--border-soft);background:var(--bg-2);}
.aff.locked .aff-hero .pk{color:var(--text-faint);}
.aff.locked .aff-hero h3{color:var(--text-dim);}
.aff.locked .aff-stat{background:var(--bg-2);border-color:var(--border-soft);}
.aff.locked .aff-stat .v{color:var(--text-faint);}
.aff.locked .aff-media .btn{opacity:.5;pointer-events:none;}
.aff-locked-note{
  font-family:'IBM Plex Mono',monospace;font-size:9px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--warn);opacity:.85;margin-top:11px;
}

/* The ledger's running total. `table.data tr:last-child td` clears the bottom border on the
   last row of each section, which would leave the footer sitting flush against the body — so
   the separation is put back here, on the one table in the app that has a footer. */
table.data tfoot td{border-top:1px solid var(--border);padding-top:13px;}
table.data tfoot td.num{font-family:'IBM Plex Mono',monospace;white-space:nowrap;}

/* Two rates and three payout states, so a ledger row now carries two more
   columns than money. The rate is set in mono because it is a figure; the
   under-pill line is the date the hold ends, or the date a payout went out. */
.aff-rate{font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--text-dim);white-space:nowrap;}
.aff-when{
  font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--text-faint);
  margin-top:5px;white-space:nowrap;
}
/* The payout-split cards sit under the three headline ones and are secondary
   to them, so the figure is smaller and carries a line of explanation. */
.aff-stat .v.small{font-size:19px;}
.aff-stat .aff-note{font-size:10.5px;color:var(--text-faint);margin-top:6px;line-height:1.45;}
.aff-sec table.data td .btn{margin-top:0;white-space:nowrap;}

/* ---- Enroll a Student ----
   The closer's enrollment tool, on /internal. Built on the same primitives the Affiliate
   Portal is — a hero card, a wrapping form grid, a `table.data` — because it is the same
   kind of panel: one short form and a list of what it has produced. Only the pieces that
   have no equivalent above are defined here. */
.enr-hero{
  background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);
  border:1px solid var(--accent-dim);border-radius:12px;padding:17px 18px;margin-bottom:18px;
}
.enr-hero .pk{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--accent-bright);
}
.enr-hero h3{font-family:'Anton',sans-serif;font-weight:400;font-size:18px;margin:7px 0 6px;}
.enr-hero p{margin:0;color:var(--text-dim);font-size:13px;line-height:1.55;}

/* Name, email, tier and the button, laid out to wrap onto a phone — the same grid the
   Owner's affiliate entry form uses, so the two tools read as one family. */
.enr-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end;}
.enr-form .btn{margin-top:0;}
.enr-form .field-label{margin-top:0;}

/* A setter's tier: shown, and not a control. The `readonly` attribute is the behaviour and
   this is the appearance; the rule itself is the server's, which is why the note beside it is
   the sentence the endpoint sent rather than one this stylesheet's page invented. */
.enr-locked{background:var(--bg-2);color:var(--text-dim);cursor:default;}
.enr-tier-note{
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.04em;line-height:1.6;
  color:var(--warn);opacity:.9;margin-top:11px;
}
.enr-existing{margin-top:14px;}
.enr-existing .link{font-weight:700;}
.enr-existing-note{margin-top:7px;color:var(--text-muted);font-size:12px;}

.enr-sec{margin-top:26px;}
.enr-sec h3{font-family:'Anton',sans-serif;font-weight:400;font-size:15px;margin:0 0 10px;}
.enr-sec table.data td .btn{margin-top:0;white-space:nowrap;}

/* The second line in a cell: the student's address under their name, and the expiry or the
   signup date under a status pill. Same treatment as .aff-when beside it. */
.enr-when{
  font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--text-faint);
  margin-top:5px;
}

/* The clipboard fallback: the whole address, on screen, selectable. Allowed to wrap — a
   truncated link is worse than an ugly one when copying it by hand is the point. */
.enr-raw{
  font-family:'IBM Plex Mono',monospace;font-size:11.5px;color:var(--text);
  background:var(--bg-2);border:1px solid var(--border);border-radius:7px;
  padding:9px 10px;margin-top:9px;word-break:break-all;user-select:all;
}

/* ---- the New Student Onboarder's Tracker ----
   One tab drawn by /assets/onboarding-tracker.js and mounted on both dashboards — the
   onboarder's own on /student, the payroll oversight view on /internal — so the classes live
   here in the shared stylesheet rather than beside either page. Built on the same primitives
   the Affiliate Portal and Enroll a Student are (a hero card, a wrapping form grid, a
   `table.data`, the `.pill` states) because it is the same kind of panel: one short form and a
   list of what it has produced. Only the pieces with no equivalent above are defined here. */
.obt-hero{
  position:relative;overflow:hidden;
  background:linear-gradient(180deg,var(--accent-wash),var(--surface) 70%);
  border:1px solid var(--accent-dim);border-radius:14px;padding:18px 20px;margin-bottom:18px;
}
/* Grain over the gradient and a lit edge along the top, the finish the Trackers tab's
   cards carry, so the onboarder's panel is made of the same material as the sheet the
   sales floor files on. Paint only: nothing here changes what the card says. */
.obt-hero::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain);mix-blend-mode:overlay;opacity:.18;
}
.obt-hero::after{
  content:"";position:absolute;left:0;right:0;top:0;height:2px;pointer-events:none;
  background:linear-gradient(90deg,var(--accent-dim),var(--accent-bright));
}
.obt-hero > *{position:relative;z-index:1;}
.obt-hero .pk{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--accent-bright);
}
.obt-hero h3{font-family:'Anton',sans-serif;font-weight:400;font-size:18px;margin:7px 0 6px;}
.obt-hero p{margin:0;color:var(--text-dim);font-size:13px;line-height:1.55;}

.obt-card{margin-bottom:18px;}
.obt-card:last-child{margin-bottom:0;}
.obt-h{
  font-family:'Anton',sans-serif;font-weight:400;font-size:16px;letter-spacing:.01em;
  margin:0 0 13px;display:flex;align-items:center;gap:11px;
}
/* The rule runs off the end of the title, as it does beside a section heading on the
   Trackers tab. Drawn on the heading itself so the markup does not have to carry an
   empty element for it. */
.obt-h::after{content:"";flex:1;border-top:1px solid var(--border-soft);}

/* The five boxes, laid out to wrap onto a phone — the same grid the Owner's affiliate entry
   form and the closer's enrollment form use, so the three tools read as one family. The
   recording link is given the full width wherever the grid has more than one column: it is a
   pasted URL and a 170px box shows none of it. */
.obt-form{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));align-items:end;}
.obt-form .btn{margin-top:0;}
.obt-form .field-label{margin-top:0;}
.obt-form-wide{grid-column:1 / -1;}
/* The shared input rule above is by type and covers neither `tel` nor `date`, so two of
   this form's five boxes were rendering as the browser's own white controls in the middle
   of a dark card. Brought into line here rather than by widening that selector for one
   panel — the same way .mem-contract-form does it for its date box. */
.obt-form input[type=tel],.obt-form input[type=date]{
  width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);
  font-family:'Work Sans',sans-serif;font-size:14.5px;padding:12px 13px;border-radius:9px;
}
.obt-form input[type=date]{font-family:'IBM Plex Mono',monospace;font-size:13.5px;}

/* The next-paycheck footer. The same shape as the Commission card's Next Payout strip on
   /internal's Trackers tab, because it answers the same question for a different seat: what is
   coming, and on what day. */
.obt-pay{
  position:relative;overflow:hidden;
  border:1px solid var(--good);border-radius:14px;padding:16px 18px;margin-bottom:18px;
  background:
    radial-gradient(110% 140% at 100% 0%, rgba(63,174,111,.16), rgba(63,174,111,0) 60%),
    linear-gradient(180deg,#0d2118,var(--surface) 85%);
}
.obt-pay::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain);mix-blend-mode:overlay;opacity:.16;
}
.obt-pay > *{position:relative;z-index:1;}
.obt-pay-k{
  font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--good);
}
.obt-pay-v{
  font-family:'Anton',sans-serif;font-size:32px;line-height:1.05;margin-top:6px;
  /* The figure the tab is opened for, so it is the one thing on it that glows. */
  text-shadow:0 0 26px rgba(63,174,111,.45);
}
.obt-pay-m{font-size:11.5px;color:var(--text-dim);margin-top:6px;line-height:1.5;}

/* The link to the recording, which is the whole evidence of the work. Left as a link and
   not dressed up as a button, but in the app's own accent rather than the browser's blue —
   the same treatment .mem-contract-value gives the one link in a contract card. */
.obt-card table.data td a{color:var(--accent-bright);}

/* The second line in a cell: the student's address under their name, and the payroll stamps
   under a status pill. Same treatment as .aff-when and .enr-when beside it. */
.obt-when{
  font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--text-faint);
  margin-top:5px;line-height:1.5;
}

/* A charged-back row stays in the list and is dimmed rather than removed — the work happened
   and the record of it is the point; what changed is that it is no longer money. */
.obt-row-cb td{opacity:.62;}

/* ---- a claimed student, waiting on its recording ----
   The card above the form, for entries an onboarder claimed in the Finalized Rev Chat rather
   than filed here. Gold-free and deliberately quiet: this is a to-do list, not a figure, and
   the loud colour on this feature belongs to the unclaimed card in the chat. */
.obt-note{margin:-4px 0 12px;color:var(--text-dim);font-size:12.5px;line-height:1.55;}

/* The two boxes that finish a claimed row, sized to the table rather than to the form grid.
   Styled here rather than borrowed from .obt-form because they sit inside a `table.data` cell,
   where a 170px minimum would push the columns off a phone. */
.obt-inline{
  width:100%;min-width:132px;background:var(--surface-2);border:1px solid var(--border);
  color:var(--text);font-family:'Work Sans',sans-serif;font-size:12.5px;
  padding:7px 9px;border-radius:8px;
}
.obt-inline::placeholder{color:var(--text-faint);}
.obt-inline:focus{outline:none;border-color:var(--accent-dim);}

/* The payroll marks. Kept on one line each so a narrow column does not split a button's label,
   and allowed to stack when the cell runs out of room. */
.obt-actions{white-space:nowrap;}
.obt-actions .btn{margin-top:0;margin-right:5px;white-space:nowrap;}
.obt-actions .btn:last-child{margin-right:0;}

/* In-place Academy membership correction, shared by both profile surfaces. */
.accttier{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-top:18px;padding:18px;border:1px solid rgba(214,169,73,.42);border-radius:12px;background:linear-gradient(135deg,rgba(214,169,73,.10),rgba(214,169,73,.025));}
.accttier h3{margin:3px 0 5px;font-size:18px;}.accttier p{margin:0;color:var(--text-dim);font-size:12.5px;line-height:1.55;}
.accttier-k{font-family:'IBM Plex Mono',monospace;font-size:9.5px;letter-spacing:.15em;text-transform:uppercase;color:#d6a949;}
.accttier-modal{position:fixed;inset:0;z-index:1100;display:grid;place-items:center;padding:20px;background:rgba(7,8,10,.82);backdrop-filter:blur(5px);}
.accttier-sheet{width:min(460px,100%);padding:24px;border:1px solid var(--border);border-radius:14px;background:var(--surface);box-shadow:0 24px 80px rgba(0,0,0,.55);}
.accttier-sheet h3{margin:6px 0 8px;font-size:23px;}.accttier-note{margin:0 0 18px;color:var(--text-dim);font-size:13px;line-height:1.55;}
.accttier-sheet select{width:100%;}.accttier-acts{display:flex;gap:8px;margin-top:18px;}.accttier-locked{overflow:hidden;}
@media(max-width:560px){.accttier{align-items:flex-start;flex-direction:column}.accttier .btn{width:100%}}
