/* ========================================================================
   GagaMuller Learning — 05-courses.css
   Course cards, the stage tracker and the course player frame.

   Loaded by index.html as sheet 5 of 7. The order is the cascade: a
   later sheet may override an earlier one, so it must not be changed.
   ======================================================================== */
  /* ---------- course cards ---------- */
  .cc{position:relative; border-radius:var(--radius); border:1px solid var(--line); background:var(--surface);
    overflow:hidden; transition:transform .2s ease, border-color .2s ease, box-shadow .2s ease;
    display:flex; flex-direction:column; text-align:left; width:100%;}
  .cc:hover{transform:translateY(-3px); border-color:color-mix(in srgb, var(--brand) 50%, var(--line));
    box-shadow:var(--shadow);}
  .cc-top{height:92px; position:relative; display:flex; align-items:flex-end; padding:13px 15px; overflow:hidden;}
  .cc-top::after{content:""; position:absolute; inset:0; background:linear-gradient(180deg,transparent 28%,rgba(0,0,0,.42));}
  .cc-ico{position:absolute; top:13px; right:15px; width:34px; height:34px; color:#fff; opacity:.92;}
  .cc-ico svg{width:100%; height:100%; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round;}
  .cc-code{position:relative; z-index:1; font:700 10.5px var(--mono); color:rgba(255,255,255,.92); letter-spacing:.5px;}
  .cc-body{padding:15px; display:flex; flex-direction:column; gap:10px; flex:1;}
  .cc-body .cd{font-size:13.5px; color:var(--muted); line-height:1.5; flex:1;}
  .cc-meta{display:flex; align-items:center; gap:11px; font:500 11.5px var(--mono); color:var(--muted); flex-wrap:wrap;}
  .cc-meta span{display:inline-flex; align-items:center; gap:5px;}
  .cc-meta svg{width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round;}
  .cc-foot{padding:12px 15px; border-top:1px solid var(--hair); background:var(--raised);
    display:flex; align-items:center; gap:11px;}

  /* ---------- stage tracker ---------- */
  .track{display:flex; align-items:center; width:100%;}
  .track .st{flex:1; display:flex; flex-direction:column; gap:7px; align-items:center; min-width:0;}
  .track .st .nd{width:23px; height:23px; border-radius:50%; border:2px solid var(--line); background:var(--surface);
    display:grid; place-items:center; font:800 10px var(--mono); color:var(--muted); z-index:1; transition:.2s;}
  .track .st.done .nd{background:var(--live); border-color:var(--live); color:var(--ink-on-strong);}
  .track .st.now .nd{border-color:var(--brand-solid); color:var(--brand); box-shadow:0 0 0 4px var(--brand-soft);}
  .track .st .lb{font:500 11px var(--display); color:var(--muted); text-align:center; line-height:1.2;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;}
  .track .ln{height:2px; flex:1; background:var(--line); margin-bottom:23px; min-width:12px;}
  .track .ln.done{background:var(--live);}
  .mtrack{display:inline-flex; gap:3px; align-items:center;}
  .mtrack i{width:16px; height:6px; border-radius:3px; background:var(--fill-2); display:block;}
  .mtrack i.done{background:var(--live);} .mtrack i.now{background:var(--brand-solid);} .mtrack i.over{background:var(--bad);}

  /* ---------- player ---------- */
  .player{position:fixed; inset:0; z-index:200; background:var(--bg); display:flex; flex-direction:column;
    overflow:hidden;
    animation:pageIn .28s cubic-bezier(.4,0,.2,1) both;
    background-image:radial-gradient(1150px 640px at 6% -14%, var(--glow-1), transparent 62%);}
  .pl-top{display:flex; align-items:center; gap:14px; padding:13px 22px; border-bottom:1px solid var(--line); background:var(--surface);}
  .pl-top .pt-t{font:700 14.5px var(--display); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .pl-top .pt-s{font:500 11.5px var(--mono); color:var(--muted); margin-top:2px;}
  /* min-height:0 is the whole fix. A flex child defaults to min-height:auto, so
     this grew to fit its content instead of shrinking, overflow-y:auto had
     nothing to act on, and the slide was simply clipped by the fixed-position
     player with no way to scroll. */
  /* The scrollbar is on the right, where a scrollbar goes. scrollbar-gutter
     reserves its width whether or not a given slide needs one, so the slide
     does not shift sideways by ten pixels as you move between a short slide
     and a tall one.

     It appears only when a slide is genuinely taller than the frame, which is
     the point: an overlay advertising a scrollbar that does nothing was
     removed once already, and this must not bring it back.

     Padding here does NOT give scroll room, which is worth writing down
     because it looks as though it should. The slide is a stretched flex item
     with min-height:100%, and against that the container's bottom padding
     never reaches the scroll range: measured at 120px it produced exactly
     zero extra travel, and the content still stopped dead against the footer.
     The room has to be part of the flow instead - see the .sl-long spacer. */
  .pl-body{flex:1; min-height:0; overflow-y:auto; display:flex; justify-content:center;
    align-items:stretch; padding:34px 24px 40px; overscroll-behavior:contain;
    scrollbar-gutter:stable;}
  /* The page behind the player keeps its own scrollbar, and a scrollbar down
     the side of a full-screen overlay that does nothing when you drag it is
     just wrong. The player is position:fixed and covers everything, so while
     it is open the document does not scroll and does not advertise that it
     can. Restored on close. */
  body.player-open{overflow:hidden;}
  /* An imported package, framed by the player rather than covering it. */
  .pl-body.scorm-body{padding:18px; display:flex; flex-direction:column; overflow:hidden;}
  .pl-body.scorm-body #scormFrame{flex:1; width:100%; min-height:0; border:0; background:transparent;
    border-radius:var(--radius-s); box-shadow:0 10px 34px rgba(0,0,0,.28);}
  @media (max-width:640px){ .pl-body.scorm-body{padding:9px;} }
  .pl-body::-webkit-scrollbar{width:10px;}
  .pl-body::-webkit-scrollbar-thumb{background:var(--fill-2); border-radius:99px; border:2px solid var(--bg);}
  .pl-body::-webkit-scrollbar-thumb:hover{background:var(--brand-soft);}
  .pl-body::-webkit-scrollbar-track{background:transparent;}
  /* A slide fills the frame it is given.

     With one content block per slide, a short paragraph used to sit as a single
     stranded line at the top of an otherwise empty screen. The fix is not to
     stretch the text - a 90-character measure is unreadable - but to size the
     type to how much there is to say, and to centre a short slide in the space.
     Three densities, set by slideStep() from the actual content length. */
  .slide{width:100%; max-width:840px; margin:0 auto; display:flex; flex-direction:column;}
  /* The transition is on the SLIDE, not the shell. Animating the whole player
     meant the overlay itself faded, briefly showing the app underneath. */
  .slide-in-fwd{animation:slideFwd .34s cubic-bezier(.22,1,.36,1) both;}
  .slide-in-back{animation:slideBack .34s cubic-bezier(.22,1,.36,1) both;}
  @keyframes slideFwd{from{opacity:0; transform:translate3d(26px,0,0);} to{opacity:1; transform:none;}}
  @keyframes slideBack{from{opacity:0; transform:translate3d(-26px,0,0);} to{opacity:1; transform:none;}}

  /* A question fills the slide the way a short content slide does. */
  /* safe center, not center. A centred flex column that overflows spills off
     BOTH ends, and the part above the top is unreachable - no amount of
     scrolling gets to it, because the scroll range starts below it. "safe"
     tells the browser to fall back to flex-start the moment that would
     happen, so a short slide is still centred and a tall one is readable. */
  .slide.sl-quiz{justify-content:safe center; min-height:100%; gap:2px;}

  /* Marked paper */
  .mk{border:1px solid var(--line); border-left-width:3px; border-radius:var(--radius-s);
    background:var(--raised); padding:13px 15px;}
  .mk.ok{border-left-color:var(--ok);}
  .mk.bad{border-left-color:var(--bad);}
  .mk-h{display:flex; align-items:center; gap:9px; margin-bottom:8px;}
  .mk-i{display:grid; place-items:center; flex:none;}
  .mk-i svg{width:17px; height:17px; fill:none; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round;}
  .mk.ok .mk-i{color:var(--ok);} .mk.bad .mk-i{color:var(--bad);}
  .mk-n{font:700 10.5px var(--mono); letter-spacing:0.45px; color:var(--muted); flex:1;}
  .mk-q{font-size:14.5px; font-weight:600; margin-bottom:10px; line-height:1.45;}
  .mk-a{display:flex; gap:8px; align-items:baseline; font-size:13.5px; margin-bottom:3px;}
  .mk-a .mini{min-width:104px; flex:none;}
  .mk-x{margin-top:9px; padding-top:9px; border-top:1px solid var(--hair);
    font-size:13.5px; color:var(--muted); line-height:1.6;}
  .slide.sl-quiz .q-t{font:600 clamp(20px, 3.2vh, 30px)/1.35 var(--display); margin:6px 0 20px;}
  .slide.sl-quiz .q-opts{display:flex; flex-direction:column; gap:10px;}
  .slide.sl-quiz .q-o{padding:15px 17px; font-size:clamp(14.5px, 1.9vh, 17px);}
  .slide.sl-quiz .q-exp{margin-top:16px; font-size:14.5px;}
  .slide.sl-quiz .q-hint{margin-top:14px; color:var(--muted);}
  /* Title slide: centred both ways, and the module kicker is dropped so the
     title is the only thing on it. */
  .slide.sl-title{justify-content:safe center; align-items:center; text-align:center; min-height:100%; gap:10px;}
  .slide.sl-title .kicker, .slide.sl-title > h2{display:none;}
  .tblk h1{font:800 clamp(34px, 7.5vh, 76px)/1.05 var(--display); letter-spacing:-2px; margin:0; color:var(--ink);}
  .tblk p{font-size:clamp(16px, 2.6vh, 24px); color:var(--muted); margin:14px auto 0; max-width:46ch; line-height:1.5;}
  .slide.sl-short{justify-content:safe center; min-height:100%; gap:4px;}
  .slide.sl-mid{justify-content:safe center; min-height:100%;}
  .slide.sl-long{justify-content:flex-start;}
  /* Tail room under a long slide.

     A slide that holds a video or a set of cards ended its scroll on the exact
     bottom edge of the last block, so the tallest thing on the page could only
     ever be read jammed against the footer. This is a real flex item - the one
     thing that does count toward the scroll range here - so there is somewhere
     to scroll TO, and the video comes up into the middle of the frame with air
     beneath it.

     Scoped to sl-long on purpose. slideDensity() returns sl-long for anything
     "heavy" - a video with a URL, reveal cards, numbered steps, stat tiles, a
     scenario, a do/don't - which is precisely the set that can outgrow the
     frame. The centred densities are short by definition and would simply be
     pushed off centre by it. */
  .slide.sl-long::after{content:''; display:block; flex:none; height:clamp(36px, 7vh, 72px);}

  .slide h2{font-size:clamp(27px, 3.4vh, 40px); letter-spacing:-.6px; margin-bottom:9px; line-height:1.15;}
  /* Headings and the module kicker are centred; body copy is not.

     Deliberately scoped to the slide's own heading rather than every h1-h4 in
     the content. The h4s inside a callout, a do/don't pair or a numbered step
     are labels on a card, sitting directly above left-aligned text in the same
     box - centring those breaks the reading line rather than tidying it. If
     those are wanted centred too, add them here rather than widening this
     selector, so the decision stays visible. */
  .slide > h2, .slide > .kicker{text-align:center;}
  .slide > h2{margin-left:auto; margin-right:auto; max-width:34ch;}
  .slide.sl-short h2{font-size:clamp(32px, 5vh, 54px); letter-spacing:-1.2px;}
  .slide .sub{color:var(--muted); font-size:14.5px; margin-bottom:24px;}
  .slide p{font-size:15px; line-height:1.68;}
  .slide .lead{font-size:17px; color:var(--muted); line-height:1.6;}

  /* Short slides get display-scale body text, so a single statement reads as a
     deliberate one rather than as a fragment someone forgot to finish. */
  .slide.sl-short p{font-size:clamp(17px, 2.5vh, 26px); line-height:1.5;}
  .slide.sl-short .lead{font-size:clamp(19px, 2.9vh, 30px); line-height:1.45; color:var(--ink);}
  .slide.sl-mid p{font-size:clamp(15px, 2vh, 18px);}
  .slide.sl-mid .lead{font-size:clamp(17px, 2.2vh, 21px);}

  /* ---------- where the author put it ---------------------------------
     Placement, set per slide in the deck editor and absent everywhere else, so
     a slide nobody has laid out is styled exactly by the density rules above.

     PLACED AFTER THOSE RULES ON PURPOSE. .slide.pl-vp-c and .slide.sl-long are
     both (0,2,0), so specificity settles nothing and SOURCE ORDER is what
     decides. Moving this block above them would silently stop it working.

     `al` is text-align and nothing else. align-items would have been the
     obvious reach and it is wrong: .slide is a flex column, so align-items
     shrinks its children to fit their content - a callout, a stat grid or a
     do/don't pair would collapse out of its box rather than move. Text moves;
     boxes keep their width.

     .slide.pl-al-l > h2 is (0,2,1) against the (0,1,1) of the centring rule
     above, so the heading follows the slide without needing !important. */
  .slide.pl-al-l{text-align:left;}   .slide.pl-al-l > h2, .slide.pl-al-l > .kicker{text-align:left;}
  .slide.pl-al-c{text-align:center;} .slide.pl-al-c > h2, .slide.pl-al-c > .kicker{text-align:center;}
  .slide.pl-al-r{text-align:right;}  .slide.pl-al-r > h2, .slide.pl-al-r > .kicker{text-align:right;}
  /* A centred heading is held to a 34ch measure by margin:auto above. Left and
     right have to give that back, or the heading stays in a centred column and
     only its text moves inside it. */
  .slide.pl-al-l > h2{margin-left:0; margin-right:auto;}
  .slide.pl-al-r > h2{margin-left:auto; margin-right:0;}

  /* ---------- per-item alignment --------------------------------------
     One item's own horizontal alignment, overriding the slide default for that
     item alone - the lever that lets an opening line read from the left while
     the module heading above it stays centred, on the same slide.

     It is applied to the item's OWN box: a flowing block wrapped in
     .pl-ba-item, a placed block's .dk-pos, or the heading in .dk-head-al /
     .dk-pos. Because these sit on the element itself rather than on .slide,
     they beat the slide-scoped .slide.pl-al-* without !important, and an item
     that carries no `ba` simply inherits the slide as before.

     text-align only, for the same reason the slide version is: the slide is a
     flex column and align-items would collapse a callout or a grid out of its
     box. Text moves; boxes keep their width. The heading's own measure is given
     back the same way the slide rule gives it back. */
  .pl-ba-l, .dk-head-al.pl-ba-l, .slide.dk-free .dk-pos.pl-ba-l{text-align:left;}
  .pl-ba-c, .dk-head-al.pl-ba-c, .slide.dk-free .dk-pos.pl-ba-c{text-align:center;}
  .pl-ba-r, .dk-head-al.pl-ba-r, .slide.dk-free .dk-pos.pl-ba-r{text-align:right;}
  .pl-ba-l .kicker, .pl-ba-l h1, .pl-ba-l h2{text-align:left;}
  .pl-ba-c .kicker, .pl-ba-c h1, .pl-ba-c h2{text-align:center;}
  .pl-ba-r .kicker, .pl-ba-r h1, .pl-ba-r h2{text-align:right;}
  .dk-head-al.pl-ba-l h2, .dk-pos.pl-ba-l h2{margin-left:0; margin-right:auto;}
  .dk-head-al.pl-ba-c h2, .dk-pos.pl-ba-c h2{margin-left:auto; margin-right:auto;}
  .dk-head-al.pl-ba-r h2, .dk-pos.pl-ba-r h2{margin-left:auto; margin-right:0;}

  /* min-height travels with these. .sl-long deliberately does not have it, and
     without it there is no spare room for justify-content to distribute, so
     "bottom" would sit wherever the content happened to end.

     safe, for the reason documented up at .sl-quiz: a centred or end-packed
     flex column that overflows spills off BOTH ends, and the part above the top
     is unreachable at any scroll position. safe falls back to flex-start the
     moment that would happen. */
  .slide.pl-vp-t{justify-content:flex-start; min-height:100%;}
  .slide.pl-vp-c{justify-content:safe center; min-height:100%;}
  .slide.pl-vp-b{justify-content:safe flex-end; min-height:100%;}

  /* ---------- freely placed blocks ------------------------------------
     A slide with at least one block moved and resized on it becomes a fixed-shape
     canvas the blocks are parked on absolutely, the way items sit on a page.
     Every course written before the deck editor renders exactly as before,
     because none carry the class - slideStep() adds it only when a block on the
     slide has a `pos`.

     A FIXED 16:9 SHAPE, not min-height. A percentage top needs a height to be a
     percentage OF, and a slide's height depends on its content - so a canvas
     whose height came from its content would move a block's top every time the
     content changed, which is exactly the glitch. Giving the canvas a shape of
     its own makes every percentage stable: the author places against the same
     box the learner sees, at any width. */
  .slide.dk-free{position:relative; width:100%; max-width:960px; aspect-ratio:16 / 9;
    min-height:0; margin:0 auto; justify-content:flex-start; overflow:hidden;}
  .slide.dk-free > .kicker, .slide.dk-free > h2{position:relative; z-index:1;}
  /* A placed block sits absolutely; its content is zoomed by --dk-scale so the
     learner sees the block at the size the author left it - text and all. The
     inner is laid out at 1/scale of the box and transform-scaled back up. */
  .slide.dk-free .dk-pos{position:absolute; box-sizing:border-box; overflow:hidden;}
  /* The content is laid out at 1/scale of the box and transform-scaled back up,
     and then CENTRED IN ITS BOX exactly as the deck editor centres it - see
     .dk-item.placed .dk-item-in in 08-deck-canvas.css. Without this the editor
     vertically centred a placed block while the player pinned it to the top, so
     a slide an author laid out one way previewed another: the "text is messed
     up / not where I put it" report. The two wrappers now share one layout, so
     the stage is a faithful preview of the slide. */
  .slide.dk-free .dk-pos-in{width:calc(100% / var(--dk-scale, 1));
    height:calc(100% / var(--dk-scale, 1)); transform:scale(var(--dk-scale, 1));
    transform-origin:top left; display:flex; flex-direction:column; justify-content:center;}
  .slide.dk-free .dk-pos > *, .slide.dk-free .dk-pos-in > *{margin:0;}

  /* Wider measure only where the content is a grid rather than prose. */
  .slide:has(.flips), .slide:has(.grid), .slide:has(.steps), .slide:has(.stats){max-width:1040px;}

  .slide .kicker{font:700 10.5px var(--mono); letter-spacing:0.55px; color:var(--brand); margin-bottom:9px;}
  .slide.sl-short .kicker{margin-bottom:14px;}

  /* Blocks inside a short slide grow to share the room rather than clustering at
     the top with a void beneath them. */
  .slide.sl-short > .callout, .slide.sl-mid > .callout{padding:20px 22px;}
  .slide.sl-short > .callout h4{font-size:16.5px;}
  .slide.sl-short > .callout p{font-size:clamp(15px, 2vh, 19px);}
  /* A video is a fixed 16:9 box with an absolutely positioned iframe inside.
     It was first made a flex child that GREW, which stretched the container
     while the aspect box kept its own ratio and the bottom ran off screen.
     The answer then was to cap its width from the viewport height so it could
     never be taller than the frame - which fixed the overflow by making the
     video small, and on a short screen, very small: 112px tall on a 620px
     viewport, with a file link under it, is not a video anybody can watch.

     Both attempts were solving the wrong problem. A slide that holds a video
     AND the documents it refers to is simply taller than the frame sometimes,
     and the honest answer to that is to let it scroll - which .pl-body has
     always been able to do. So the cap is now a floor as well: the video gets
     a real size, and if that makes the slide too tall, the slide scrolls. */
  /* THE EDITOR'S WRAPPER IS TRANSPARENT TO THIS RULE, and that is what makes
     the stage a true preview rather than a near one.

     Every rule written as `.slide > X` silently stops applying in the editor,
     because the editor wraps each block in .dk-item > .dk-item-in so that it can
     be selected, moved and resized. For the video that was not cosmetic: a
     video's frame is an aspect-ratio box whose iframe is absolutely positioned,
     so it contributes NOTHING to its parent's intrinsic width. Without a width
     coming from here, .dk-item fell back to fit-content of whatever was left -
     the caption, or nothing at all - and the video rendered as a sliver. The
     same block measured 840x515 for the learner and 756x487 in the editor, and
     collapsed completely when it had no caption.

     So the selector names both places the block can legitimately sit. Anything
     added here has to do the same, which is the whole trick to this editor
     being WYSIWYG: one rule, both contexts, no second copy to drift.

     A DESCENDANT selector, not a child one: the editor's chain is
     .dk-item > .dk-item-in > .dk-video-preview > .vid, and writing `>` here
     reproduced the original bug one level further down. */
  .slide > .vid, .slide .dk-item-in .vid{width:100%; margin:18px auto;
    max-width:min(100%, max(560px, calc(62vh * 16 / 9)));}
  /* Nothing on a slide may be squeezed to make it fit.

     Every direct child of .slide is a flex item, and a flex item shrinks by
     default. With min-height:100% on the centred densities, a slide that was
     slightly too tall did not overflow - it compressed whatever was most
     compressible, which is the aspect-ratio box holding the video. That is why
     the video lost height as the window got shorter instead of the slide
     growing a scrollbar. flex:none means the content keeps its size and the
     overflow becomes real, where the scroll can deal with it. */
  .slide > *, .slide .dk-item-in > *{flex:none;}
  /* The native control bar is hidden to prevent skipping, so the play control is
     ours. Sits over the frame, bottom-left. */
  .vid .vf{position:relative;}
  .vid .vid-play{position:absolute; left:12px; bottom:12px; z-index:2;}
  /* Part of the column, not floating over it. It was absolutely positioned with
     the body reserving 118px of padding to clear it - which works until the
     buttons wrap on a narrow window and the footer grows past the reservation,
     covering the bottom of whatever you were reading. As a flex sibling it
     cannot overlap the body at any width. */
  .pl-foot{flex:none; padding:15px 22px; border-top:1px solid var(--line);
    background:var(--surface); display:flex; align-items:center; gap:15px; flex-wrap:wrap;}
  .pl-prog{flex:1; max-width:340px;}
  .pl-prog .pp-l{font:500 11px var(--mono); color:var(--muted); margin-bottom:6px;}
  .pl-dots{display:flex; gap:4px; align-items:center; flex-wrap:wrap;}
  .pl-dots i{width:22px; height:4px; border-radius:999px; background:var(--fill-2); transition:.3s; cursor:pointer;}
  .pl-dots i.done{background:var(--live);}
  .pl-dots i.on{background:var(--brand-solid); width:31px;}

/* ======================= departments, down the page ======================
   Courses and Paths are one library seen two ways, and both are organised by
   department. A dropdown hid the shape of it: you had to open a menu to learn
   which departments hold anything, and a strip of tabs across the top put
   seventeen departments on one line and made the eye work sideways.

   So the departments are the page: one wide bubble each, stacked, carrying its
   own count, and the courses inside it appear when it is opened. Closed, the
   whole library is one scannable list. Each opens independently, because the
   question is usually "how do these two compare" rather than "show me one".
   ======================================================================== */
.dept-acc{display:flex; flex-direction:column; gap:11px; margin-bottom:18px;}
/* --dept and --dept-soft are set inline per group, from admin/dept-colours.js.
   The fallbacks are here so a bubble drawn without them is still a bubble. */
.dept-grp{--dept:var(--brand); --dept-soft:var(--brand-soft);}

.dept-bub{display:flex; align-items:center; gap:13px; width:100%;
  padding:16px 19px; border-radius:15px; cursor:pointer; text-align:left;
  border:1px solid var(--line); background:var(--raised); color:var(--txt);
  font-family:inherit; line-height:1.25;
  transition:border-color .2s ease, background .2s ease, color .2s ease,
             border-radius .2s ease;}
.dept-bub:hover{border-color:var(--dept);}
.dept-bub .nm{flex:1; min-width:0; font-size:15.5px; font-weight:700;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.dept-bub .sub{display:block; margin-top:3px; font-size:11.5px; font-weight:500;
  color:var(--muted); letter-spacing:.1px;}
.dept-bub .n{display:inline-grid; place-items:center; min-width:30px; height:26px;
  padding:0 9px; border-radius:999px; background:var(--fill); color:var(--muted);
  font-size:12.5px; font-weight:700; font-variant-numeric:tabular-nums; flex:none;
  transition:background .2s ease, color .2s ease;}
.dept-bub .cv{width:17px; height:17px; flex:none; opacity:.5;
  transition:transform .26s cubic-bezier(.25,.8,.3,1), opacity .2s ease;}

/* Open. The bubble keeps its box and squares off its bottom corners so it reads
   as the lid of the panel under it rather than as a separate control. */
.dept-bub.on{background:var(--dept-soft); border-color:var(--dept); color:var(--dept);
  border-bottom-left-radius:0; border-bottom-right-radius:0;}
.dept-bub.on .n{background:var(--dept); color:#fff;}
.dept-bub.on .sub{color:var(--dept); opacity:.75;}
.dept-bub.on .cv{transform:rotate(180deg); opacity:1;}

/* ---- opening and closing -------------------------------------------------
   The panel is always in the page; this is what hides it. A height nobody has
   measured cannot be transitioned to, so the fold is a one-row grid moving
   between 0fr and 1fr - which CAN be - and the clip inside it holds the
   overflow. The panel's own padding and border sit one level deeper again, or
   they would keep the shut state 32px tall.

   visibility, not just opacity: a shut panel must be unreachable by tab and
   silent to a screen reader, and it is held visible for the length of the
   collapse so the content does not vanish before the panel has finished
   closing around it. */
.dept-fold, .lib-fold{display:grid; grid-template-rows:0fr;
  transition:grid-template-rows .26s cubic-bezier(.25,.8,.3,1);}
.dept-grp.on > .dept-fold, .lib-card.on > .lib-fold,
.lib-sec.on > .lib-fold{grid-template-rows:1fr;}
.dept-clip, .lib-clip{overflow:hidden; min-height:0;
  opacity:0; visibility:hidden;
  transition:opacity .16s ease, visibility 0s linear .26s;}
.dept-grp.on > .dept-fold > .dept-clip,
.lib-card.on > .lib-fold > .lib-clip,
.lib-sec.on > .lib-fold > .lib-clip{opacity:1; visibility:visible;
  transition:opacity .22s ease .05s, visibility 0s;}

.dept-body{padding:15px; border:1px solid var(--dept); border-top:0;
  border-radius:0 0 15px 15px; background:var(--fill-2);}
.dept-body > .grid{margin-bottom:0;}
.dept-body .mini.empty{color:var(--muted); padding:6px 2px;}

/* ---- a course or path card ----------------------------------------------
   Collapsed to its header on arrival. The full card is a strip of figures, a
   row of badges and up to seven buttons; twenty of those stacked inside a
   department is a page nobody scans, and the header alone - icon, title,
   state, code - is what somebody is looking for when they arrive.

   The header is the hit area rather than just the chevron: a 28px target is
   small, and a bar that reads as one control should behave as one. */
.lib-card{padding:0; overflow:hidden;}
/* EACH CARD ITS OWN HEIGHT. A grid stretches its items to the tallest in the
   row by default, so opening one card grew every card beside it into a tall
   empty box - which reads as the whole row opening at once, and left the
   header's hover fill covering only the top of a box it no longer matched. */
/* .lib-grid once set align-items:start of its own; .grid does it for every
   card grid now. The class stays as the hook the library marks its grids with. */

.lib-head{display:flex; align-items:flex-start; gap:12px; padding:15px 16px;
  cursor:pointer; border-radius:calc(var(--radius) - 1px);
  transition:background .16s ease;}
.lib-head:hover{background:var(--fill);}
.lib-card.on > .lib-head{border-bottom-left-radius:0; border-bottom-right-radius:0;}
.lib-head h3{font-size:14.5px; line-height:1.3; margin:0;}
/* One fact per line. Run together with middots they wrapped wherever the card
   happened to end, breaking the line in the one place it means nothing. */
.lib-meta{margin-top:6px; display:flex; flex-direction:column; gap:2px;
  font-size:11.5px; line-height:1.45; color:var(--muted);}
.lib-meta .mono{font-family:var(--mono); letter-spacing:.2px;}
.lib-meta span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}

.lib-x{flex:none; width:28px; height:28px; border-radius:9px; display:grid;
  place-items:center; color:var(--muted); border:1px solid transparent;
  transition:background .16s ease, border-color .16s ease, color .16s ease;}
.lib-head:hover .lib-x{background:var(--raised); border-color:var(--line); color:var(--ink);}
.lib-x .cv{width:16px; height:16px;
  transition:transform .26s cubic-bezier(.25,.8,.3,1);}
.lib-card.on .lib-x .cv{transform:rotate(180deg);}
.lib-body{padding:2px 16px 16px;}

/* ---- a code family inside a bubble --------------------------------------
   SEC, OB, XLS. Drawn only where two or more courses share one - a heading
   over a single course costs a line and says nothing. */
.lib-sec + .lib-sec{margin-top:16px;}
.lib-sec-h{display:flex; align-items:center; gap:9px; width:100%; text-align:left;
  padding:8px 10px; border-radius:10px; cursor:pointer;
  border:1px solid transparent; transition:background .16s ease, border-color .16s ease;}
.lib-sec-h:hover{background:var(--fill); border-color:var(--line);}
.lib-sec-h .mono{font:700 11.5px var(--mono); letter-spacing:.7px; color:var(--brand);}
/* A renamable heading reads as text you can get at, and stops the fold
   toggling under somebody aiming at it - see wiring/code-family.js. */
.lib-sec-h .fam-name{cursor:text; border-radius:6px; padding:2px 5px; margin:-2px -5px;
  transition:background .14s ease;}
.lib-sec-h:hover .fam-name{background:var(--fill-2);}
/* The code kept beside a renamed heading: somebody reading "Security" still
   has to be able to tell which codes sit under it. */
.lib-sec-h .fam-code{letter-spacing:.6px; opacity:.75;}
.fam-edit{font:700 11.5px var(--mono); letter-spacing:.7px; color:var(--brand);
  background:var(--surface); border:1px solid var(--brand); border-radius:6px;
  padding:3px 6px; width:14ch; max-width:40vw;}
.fam-edit:focus{outline:2px solid var(--brand); outline-offset:1px;}
.lib-sec-h .mini{margin-left:auto;}
.lib-sec-h .cv{width:15px; height:15px; flex:none; opacity:.55;
  transition:transform .26s cubic-bezier(.25,.8,.3,1);}
.lib-sec.on > .lib-sec-h .cv{transform:rotate(180deg); opacity:.9;}
/* The rule under the heading belongs to the open state: closed, it would be a
   line with nothing under it. */
.lib-sec.on > .lib-sec-h{border-bottom:1px solid var(--hair);
  border-bottom-left-radius:0; border-bottom-right-radius:0;}
.lib-sec > .lib-fold > .lib-clip > .grid{padding-top:11px;}

@media (prefers-reduced-motion: reduce){
  .dept-bub, .dept-bub .cv, .dept-fold, .dept-clip,
  .lib-head, .lib-x, .lib-x .cv, .lib-fold, .lib-clip,
  .lib-sec-h, .lib-sec-h .cv{transition:none;}
}
