/* ==========================================================================
   3andB · THE CURRICULUM ASSISTANT
   --------------------------------------------------------------------------
   Two placements of one component:

     .chatdock        the floating launcher and its panel, on every public page
     .chatpage        /chat, where the same panel gets the whole screen

   Everything visual comes from the tokens in main.css §1 — this file adds no
   colours of its own, so a change to the palette reaches the assistant too.

   Loaded on every page from base.html, after main.css. It is ~9KB; the
   alternative was another 400 lines in a 1200-line stylesheet, and this
   component is self-contained enough to keep to itself.

   FOUR THINGS THAT ARE NOT DECORATION AND SHOULD NOT BE "TIDIED":
     * dvh, not vh, on the mobile sheet. vh on iOS is the viewport with the
       browser chrome pretending not to exist, which puts the composer under
       the address bar exactly when the keyboard is up.
     * env(safe-area-inset-*) on the dock and the sheet. Without it the send
       button sits under the home indicator on every notched phone.
     * overscroll-behavior on the log. Without it, flicking to the top of the
       conversation scrolls the page behind the panel instead.
     * 16px on .composer__in below 620px. Anything smaller and mobile Safari
       zooms the page in when the field takes focus, which moves the sheet.

   And the sheet's position on a phone is not in this file at all: chat.js pins
   it to the visual viewport, because CSS cannot see where the keyboard has
   pushed that. The inset:0 in §8 is the pre-JS fallback, not the answer.
   ========================================================================== */

/* ========================================================================
   0 · UTILITY
   ======================================================================== */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ========================================================================
   1 · THE DOCK AND ITS LAUNCHER
   ======================================================================== */
.chatdock{position:fixed;z-index:80;right:clamp(14px,2.4vw,26px);
  bottom:calc(clamp(14px,2.4vw,26px) + env(safe-area-inset-bottom,0px));
  display:flex;flex-direction:column;align-items:flex-end;gap:.7rem}

/* A link, not a button — see the comment in _chat_widget.html. */
.chatdock__fab{display:inline-flex;align-items:center;gap:.6rem;
  font-weight:700;font-size:.92rem;color:#fff;background:var(--ink);
  padding:.72rem 1.15rem .72rem .95rem;border-radius:99px;
  box-shadow:var(--shadow-h);transition:transform .16s ease,background .16s ease}
.chatdock__fab:hover{transform:translateY(-2px);background:var(--ink-2)}
.chatdock__fabicon{display:grid;place-items:center;color:var(--green-hi)}
.chatdock__fab[data-open]{display:none}

/* The panel. Sized to sit above the fold on a laptop without covering the
   page it is answering questions about. */
.chatdock__panel{width:min(410px,calc(100vw - 28px));
  height:min(660px,calc(100dvh - 130px));
  display:flex;flex-direction:column;overflow:hidden;
  background:var(--paper-card);border:1.5px solid var(--line);
  border-radius:var(--r-lg);box-shadow:var(--shadow-h);
  animation:chat-in .18s ease-out}
@keyframes chat-in{from{opacity:0;transform:translateY(10px) scale(.99)}to{opacity:1;transform:none}}

.chatdock__hd{display:flex;align-items:center;justify-content:space-between;gap:.8rem;
  padding:.85rem 1rem;background:var(--ink);color:#fff;flex:0 0 auto}
.chatdock__title{font-family:var(--display);font-weight:800;font-size:1rem;letter-spacing:-.02em}
.chatdock__sub{font-size:.76rem;opacity:.72;margin-top:.1rem}
.chatdock__acts{display:flex;gap:.25rem;flex:0 0 auto}
.chatdock__act{display:grid;place-items:center;width:32px;height:32px;border-radius:8px;
  color:#fff;font-size:.95rem;line-height:1;opacity:.8;transition:background .14s ease,opacity .14s ease}
.chatdock__act:hover{background:rgba(255,255,255,.14);opacity:1}

/* ========================================================================
   2 · THE PANEL BODY (shared by the dock and the page)
   ======================================================================== */
.chatui{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;
  background:var(--paper-card)}

.chatui__log{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  padding:1.05rem 1rem 0;display:flex;flex-direction:column;gap:.85rem;
  scroll-behavior:smooth}

.chatui__foot{flex:0 0 auto;padding:0 1rem calc(.85rem + env(safe-area-inset-bottom,0px));
  font-size:.72rem;line-height:1.45;color:var(--text-soft)}
.chatui__foot a{text-decoration:underline;text-underline-offset:2px}

/* ========================================================================
   3 · MESSAGES
   ======================================================================== */
.msg{max-width:100%;display:flex;flex-direction:column;gap:.3rem;
  animation:msg-in .2s ease-out}
@keyframes msg-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

.msg__who{font-family:var(--body);font-weight:700;font-size:.66rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--text-soft)}

.msg__body{font-size:.94rem;line-height:1.6}
.msg__body p+p{margin-top:.6rem}
.msg__body ul{margin:.5rem 0 0;padding-left:1.05rem;display:flex;flex-direction:column;gap:.28rem}
.msg__body li{font-size:.92rem}
.msg__body strong{font-weight:700;color:var(--ink)}
.msg__body a{text-decoration:underline;text-underline-offset:2px;font-weight:600}

/* The assistant. A left rule in brand green rather than a bubble: a chat panel
   full of grey speech bubbles reads as somebody else's product, and the answers
   here are usually three lines of prose with a card under them. */
.msg--bot .msg__body{border-left:3px solid var(--green);padding-left:.8rem}
.msg--greeting .msg__body{color:var(--text)}

/* The visitor. Right-aligned and filled, so the eye can find the question a
   long answer belongs to while scrolling back. */
.msg--me{align-items:flex-end}
.msg--me .msg__body{background:var(--ink);color:#fff;padding:.6rem .85rem;
  border-radius:14px 14px 4px 14px;max-width:min(88%,44ch);white-space:pre-wrap}
.msg--me .msg__who{display:none}

.msg--error .msg__body{border-left-color:var(--red);color:var(--red)}

/* Thinking. Three dots that stay put — an animated ellipsis that reflows the
   line is the one animation in a chat panel everybody notices. */
.msg--typing .msg__body{display:flex;align-items:center;gap:.5rem;color:var(--text-soft);
  font-size:.85rem}
.dots{display:inline-flex;gap:3px}
.dots i{width:5px;height:5px;border-radius:50%;background:var(--green);
  animation:dot 1.1s infinite ease-in-out}
.dots i:nth-child(2){animation-delay:.15s}
.dots i:nth-child(3){animation-delay:.3s}
@keyframes dot{0%,60%,100%{opacity:.25;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}

/* ========================================================================
   4 · WORKBOOK CARDS
   ======================================================================== */
/* What makes this panel worth building rather than embedding somebody else's:
   the assistant cites a slug, the server resolves it against the database, and
   what lands under the paragraph is the real book with real buttons. The model
   never writes a URL, so it cannot write a wrong one. */

.wbcards{display:flex;flex-direction:column;gap:.6rem;margin-top:.15rem}

.wbcard{display:grid;grid-template-columns:58px minmax(0,1fr);gap:.75rem;
  padding:.7rem;background:var(--paper);border:1.5px solid var(--line);
  border-left:4px solid var(--c,var(--ink));border-radius:var(--r);
  transition:border-color .14s ease,transform .14s ease}
.wbcard:hover{transform:translateY(-1px);border-color:var(--c,var(--ink))}

.wbcard__cover{width:58px;aspect-ratio:3/4;border-radius:5px;overflow:hidden;
  background:var(--paper-2);display:grid;place-items:center}
.wbcard__cover img{width:100%;height:100%;object-fit:cover}
.wbcard__nocover{font-family:var(--display);font-weight:800;font-size:.6rem;
  color:var(--text-soft);text-align:center;padding:.2rem;line-height:1.1}

.wbcard__title{font-family:var(--display);font-weight:800;font-size:.98rem;
  line-height:1.15;letter-spacing:-.015em}
.wbcard__title a:hover{color:var(--c,var(--ink))}
.wbcard__meta{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.35rem}
.wbcard__meta .chip{font-size:.63rem}
.wbcard__flag{font-weight:700;font-size:.6rem;letter-spacing:.09em;text-transform:uppercase;
  padding:.24em .5em;border-radius:99px;background:var(--green);color:var(--green-ink)}
.wbcard__teaser{font-size:.8rem;color:var(--text-soft);line-height:1.45;margin-top:.35rem}

.wbcard__links{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.5rem}
.wblink{font-weight:700;font-size:.74rem;padding:.34em .7em;border-radius:99px;
  border:1.5px solid var(--line);background:var(--paper-card);color:var(--text);
  transition:border-color .14s ease,background .14s ease}
.wblink:hover{border-color:var(--ink);background:var(--paper-2)}
.wblink--page{background:var(--ink);border-color:var(--ink);color:#fff}
.wblink--page:hover{background:var(--ink-2);border-color:var(--ink-2)}
.wblink--sample{border-color:var(--green);color:var(--green-d)}
.wblink--sample:hover{background:rgba(18,201,60,.1);border-color:var(--green-d)}

/* ========================================================================
   5 · THE SHELF
   ======================================================================== */
/* Everything the assistant has recommended in this conversation, kept where it
   can be found again. On the page it is a sidebar; in the dock it is a strip
   that opens downward. Both are the same markup. */

.shelf{flex:0 0 auto;border-bottom:1.5px solid var(--line);background:var(--paper-2)}
.shelf__tog{display:flex;align-items:center;gap:.5rem;width:100%;
  padding:.55rem 1rem;font-weight:700;font-size:.78rem;color:var(--text)}
.shelf__n{display:grid;place-items:center;min-width:20px;height:20px;padding:0 .35em;
  border-radius:99px;background:var(--green);color:var(--green-ink);font-size:.7rem}
.shelf__label{color:var(--text-soft);font-weight:600}
.shelf__arw{margin-left:auto;color:var(--text-soft);transition:transform .16s ease}
.shelf__tog[aria-expanded=true] .shelf__arw{transform:rotate(180deg)}

.shelf__body{padding:0 1rem .7rem}
.shelf__list{display:flex;flex-direction:column;gap:.3rem;list-style:none}
.shelf__item{display:flex;align-items:center;gap:.55rem;padding:.35rem;border-radius:8px;
  background:var(--paper-card);border:1px solid var(--line)}
.shelf__item img{width:26px;aspect-ratio:3/4;object-fit:cover;border-radius:3px;flex:0 0 auto}
.shelf__name{font-size:.8rem;font-weight:600;line-height:1.25;
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.shelf__go{margin-left:auto;font-size:.72rem;font-weight:700;color:var(--text-soft);flex:0 0 auto}
.shelf__go:hover{color:var(--ink)}
.shelf__clear{margin-top:.5rem;font-size:.72rem;font-weight:700;color:var(--text-soft);
  text-decoration:underline;text-underline-offset:2px}
.shelf__clear:hover{color:var(--red)}

/* ========================================================================
   6 · SUGGESTIONS AND THE COMPOSER
   ======================================================================== */
.chatui__suggest{flex:0 0 auto;display:flex;gap:.4rem;padding:.7rem 1rem .1rem;
  overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.chatui__suggest::-webkit-scrollbar{display:none}
.chatui__suggest[hidden]{display:none}

.askchip{flex:0 0 auto;font-weight:600;font-size:.78rem;padding:.42em .85em;
  border-radius:99px;border:1.5px solid var(--line);background:var(--paper-card);
  color:var(--text);white-space:nowrap;transition:border-color .14s ease,background .14s ease}
.askchip:hover{border-color:var(--green);background:rgba(18,201,60,.08)}

.composer{flex:0 0 auto;display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem .6rem}
/* One clean line at rest, stated rather than inferred. Everything here is
   border-box, so the field's resting height is the line box plus both paddings
   plus both borders — and min-height says exactly that instead of trusting the
   textarea's own rows=1 intrinsic height, which browsers round differently and
   which chat.js then overwrote three pixels short. Either way the first line
   ends up clipped and the box reads as a line and a half.
   Change the padding or the line-height and the calc has to follow. */
.composer__in{flex:1 1 auto;min-width:0;resize:none;max-height:132px;
  font-family:var(--body);font-size:.94rem;line-height:1.5;color:var(--text);
  padding:.68rem .85rem;min-height:calc(1.5em + 1.36rem + 3px);
  border:1.5px solid var(--line);border-radius:14px;
  background:var(--paper);transition:border-color .14s ease}
.composer__in:focus{outline:none;border-color:var(--green)}
.composer__in::placeholder{color:var(--text-soft)}

/* The 16px floor is not only a phone problem. iPadOS Safari zooms a focused
   field under 16px exactly as the phone does, and the 620px breakpoint in §8
   never reaches a tablet — so the floor hangs off the pointer, not the width.
   A touchscreen laptop picking this up loses nothing. */
@media (pointer:coarse){
  .composer__in{font-size:16px}
}

/* 44px, matching the resting field either side of it. A 40px circle beside a
   47px box sat visibly high in the bar. */
.composer__send{flex:0 0 auto;display:grid;place-items:center;width:44px;height:44px;
  border-radius:50%;background:var(--green);color:var(--green-ink);font-weight:800;
  font-size:1.05rem;transition:transform .14s ease,opacity .14s ease}
.composer__send:hover{transform:translateY(-1px)}
.composer__send[disabled]{opacity:.4;transform:none;cursor:default}

/* ========================================================================
   7 · /chat — THE FULL PAGE
   ======================================================================== */
.chatpage{padding-block:clamp(1.4rem,4vw,3rem) clamp(2rem,5vw,3.5rem)}
.chatpage__hd{max-width:62ch;margin-bottom:clamp(1.1rem,2.5vw,1.6rem)}
.chatpage__h1{font-family:var(--display);font-weight:800;
  font-size:clamp(1.9rem,4.4vw,3rem);letter-spacing:-.03em;margin-top:.35rem}
.chatpage__lede{margin-top:.7rem;color:var(--text-soft);font-size:clamp(.95rem,1.3vw,1.05rem)}

/* The conversation and the shelf, side by side. The grid areas are why the
   macro can emit the shelf first — it belongs at the top of a narrow screen and
   at the right of a wide one, and neither placement should need its own copy of
   the markup. */
.chatui--page{display:grid;grid-template-columns:minmax(0,1fr) 290px;
  grid-template-areas:"log shelf" "suggest shelf" "form shelf" "foot shelf";
  /* The first row takes everything left over and the other three size to their
     content. Without minmax(0,…) the log row sizes to the conversation instead
     and the panel grows down the page, which defeats the scroller inside it. */
  grid-template-rows:minmax(0,1fr) auto auto auto;
  gap:0 1.2rem;
  height:min(72dvh,760px);min-height:460px;
  border:1.5px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow);
  padding-right:1.2rem}
.chatui--page .chatui__log{grid-area:log;padding-top:1.4rem}
.chatui--page .chatui__suggest{grid-area:suggest}
.chatui--page .composer{grid-area:form}
.chatui--page .chatui__foot{grid-area:foot}

/* The sidebar shelf: the same element, given a column. It opens itself here
   rather than being forced open in CSS — a toggle that is still in the tab
   order but has been made unclickable is a trap for a keyboard, and chat.js
   expands it on this variant the first time something lands on it. */
.chatui--page .shelf{grid-area:shelf;align-self:stretch;margin:1.4rem 0 1rem;
  border:1.5px solid var(--line);border-radius:var(--r);background:var(--paper);
  overflow-y:auto;max-height:100%}
.chatui--page .shelf__tog{padding:.7rem .8rem .5rem}
.chatui--page .shelf__body{padding:0 .8rem .8rem}

@media (max-width:900px){
  .chatui--page{grid-template-columns:minmax(0,1fr);
    grid-template-areas:"shelf" "log" "suggest" "form" "foot";
    grid-template-rows:auto auto auto auto auto;
    height:auto;min-height:0;padding-right:0}
  .chatui--page .chatui__log{height:min(62dvh,620px);min-height:340px;padding-top:1.1rem}
  .chatui--page .shelf{margin:0;border:0;border-bottom:1.5px solid var(--line);
    border-radius:var(--r-lg) var(--r-lg) 0 0;max-height:none}
  .chatui--page .shelf__tog{padding:.55rem 1rem}
  .chatui--page .shelf__body{padding:0 1rem .7rem}
}

/* ========================================================================
   8 · PHONES — THE DOCK BECOMES A SHEET
   ======================================================================== */
/* Below this width a 410px panel floating over the page is a 410px panel with
   nowhere to float, so it takes the screen. Full height rather than a
   half-sheet: the keyboard eats half of what is left, and a conversation in the
   remaining quarter is unusable. */
@media (max-width:620px){
  .chatdock{right:0;left:0;bottom:0;padding:0 0 env(safe-area-inset-bottom,0px);
    align-items:stretch}
  .chatdock:has(.chatdock__panel:not([hidden])){padding:0}

  .chatdock__fab{position:fixed;right:14px;bottom:calc(14px + env(safe-area-inset-bottom,0px));
    padding:.8rem;border-radius:50%}
  .chatdock__fabtext{display:none}

  /* inset:0 and 100dvh are the starting position and the fallback. From the
     moment chat.js runs, the sheet is pinned to the visual viewport by inline
     top/left/width/height instead — see the long comment in dock(). Those
     inline values win over these, and dock() sets right/bottom to auto so this
     rule's 0s cannot over-constrain them. */
  .chatdock__panel{position:fixed;inset:0;width:100%;height:100dvh;
    border:0;border-radius:0;animation:sheet-in .2s ease-out}
  @keyframes sheet-in{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}

  .chatdock__hd{padding:.9rem 1rem;padding-top:calc(.9rem + env(safe-area-inset-top,0px))}
  /* 44px of something to hit. A 32px close button at the top of a phone screen
     is the control people miss and then resent. */
  .chatdock__act{width:40px;height:40px;font-size:1.1rem}
  .chatui__log{padding-inline:.9rem}
  .msg__body{font-size:.95rem}
  .wbcard{grid-template-columns:50px minmax(0,1fr)}
  .wbcard__cover{width:50px}

  /* The composer reads as a bar rather than as the last thing in the scroll. */
  .composer{padding:.6rem .9rem;border-top:1.5px solid var(--line);
    background:var(--paper-card)}
  /* 16px exactly, and not a token. Mobile Safari zooms the page in on a focused
     input whose text is under 16px, and a zoomed layout viewport drags a fixed
     sheet off the screen the instant somebody starts typing — the same symptom
     the visual-viewport pinning exists to prevent, arriving by another road. */
  /* The rest of this rule pins the phone to the metrics it already had — the
     sheet composer was the one placement that looked right, and the roomier
     line-height and padding added for the desktop panel would have moved it
     for no reason. Same arithmetic in the calc, older numbers. */
  .composer__in{font-size:16px;border-radius:12px;line-height:1.45;
    padding:.62rem .8rem;min-height:calc(1.45em + 1.24rem + 3px)}
  .composer__send{width:42px;height:42px}
  .chatui__foot{padding-inline:.9rem}

  /* /chat on a phone. The log keeps a fixed height so the composer stays within
     reach of a thumb instead of sitting below a screenful of conversation. */
  .chatui--page .chatui__log{height:min(56dvh,520px);min-height:260px}
}

/* ------------------------------------------------------------------------
   THE KEYBOARD IS UP
   ------------------------------------------------------------------------
   chat.js sets data-kb on the sheet when the visual viewport loses more than
   120px, which on a phone means one thing. Half the screen is now keyboard, so
   the parts of the panel that are there to be read when nobody is typing stand
   down and give their lines to the conversation. They come back when the
   keyboard does — nothing here is a permanent removal. */
.chatdock__panel[data-kb] .chatdock__sub,
.chatdock__panel[data-kb] .chatui__suggest,
.chatdock__panel[data-kb] .chatui__foot{display:none}
.chatdock__panel[data-kb] .chatdock__hd{padding-block:.55rem}
.chatdock__panel[data-kb] .chatui__log{padding-top:.75rem;gap:.65rem}
/* No home indicator to clear: the keyboard is standing on it. */
.chatdock__panel[data-kb] .composer{padding-bottom:.6rem}

/* Tablet: the page layout keeps its sidebar down to 900px (see above), and the
   dock panel gets a little more height than a phone allows. */
@media (min-width:621px) and (max-width:900px){
  .chatdock__panel{height:min(640px,calc(100dvh - 100px))}
}

/* Landscape phones: 100dvh is ~380px and the header plus composer leave three
   lines of conversation. Drop the sub-line and tighten everything. */
@media (max-height:520px) and (max-width:900px){
  .chatdock__sub{display:none}
  .chatui__log{padding-top:.7rem;gap:.6rem}
  .chatui__suggest{display:none}
  .chatui__foot{display:none}
  /* The footer was carrying the safe-area inset for the whole panel; with it
     gone the composer is the bottom edge and inherits the job. */
  /* …unless the keyboard is what made it short, and then there is a keyboard
     between the send button and the home indicator — the [data-kb] rule above
     outranks this one and drops the inset back to nothing. */
  .composer{padding-bottom:calc(.6rem + env(safe-area-inset-bottom,0px))}
}

/* Anyone who asked for less movement gets the panel without the entrance —
   main.css §13 kills transitions globally, and these keyframes are named
   animations it also covers, but the dots are load-bearing feedback so they
   are replaced rather than removed. */
@media (prefers-reduced-motion:reduce){
  .dots i{animation:none!important;opacity:.6}
}
