/* ==========================================================================
   ILE PORTAL UI
   Shared design system for every signed-in page: student, parent, tutor and
   admin dashboards, plus the login and password-reset screens.

   Replaces assets/portal-ui.css. Load it instead of that file, not next to it.

   WHY THIS FILE EXISTS
   The old portal used Fraunces + Inter + JetBrains Mono on a lavender ground
   with 14px radii, two blurred purple background blobs and a drop shadow on
   every card. The marketing site uses Cormorant Garamond + DM Sans on cream
   with 3px radii and no card shadows at all. Signing in swapped every font,
   every colour and every corner in one click, which is why the portal read as
   a different product. This file uses the marketing type system and the
   marketing palette, and gets its depth from 1px hairlines instead of shadows.

   HOUSE RULES BAKED IN HERE
   1. One accent colour: --brand #5534A5. Never add a second.
   2. Depth comes from hairlines on stepped surfaces. Shadows are used in
      exactly two places: the primary button, and things that float above the
      page (modal, drawer, toast). Cards never have one and never lift.
   3. Hover is a flat background tint. Nothing moves in Z, nothing scales.
   4. Money, dates, IDs, reference numbers and any column of figures are set
      in IBM Plex Mono with tabular figures so they line up.
   5. Status is never colour alone. Every pill carries its text label.

   FONTS
   This file does NOT @import Google Fonts. The marketing pages load fonts with
   a <link> in <head> and so must every portal page. The old portal-ui.css used
   @import, which is why the dashboards had no font <link> of their own. Paste
   this into <head> of every page that loads this stylesheet, above the
   stylesheet link:

     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">

   That URL is a superset of the marketing one. It adds DM Sans 600 and 700,
   which the marketing site already uses but never loads, so the browser has
   been faking them. Both origins are already allowed by the CSP in
   vercel.json (style-src fonts.googleapis.com, font-src fonts.gstatic.com),
   so no header change is needed.

   MARKUP CONTRACT
   Put class="ile-portal" on <body>. The reset and the base type are scoped to
   it so this file can sit alongside ile.css on the login page without
   restyling the marketing furniture.
   ========================================================================== */


/* ==========================================================================
   1. TOKENS
   Values are lifted from the marketing site. The names are new because the
   marketing names are historical leftovers from a dark theme (--cream is the
   near-black heading ink, --dark is the off-white page background). New code
   should use the honest names below. The legacy alias block at the very
   bottom of this file keeps old --ile-* markup alive during the switchover.
   ========================================================================== */

:root {
  /* Brand. One accent. --brand is the marketing #5534A5 unchanged. */
  --brand:            #5534A5;
  --brand-hover:      #6B40E0;  /* button hover, from marketing --purple-bright */
  --brand-soft:       #EFEAF9;  /* tinted fill behind selected rows and info pills */
  --brand-tint:       rgba(85, 52, 165, 0.06);  /* hover wash on rows and ghost buttons */
  --brand-ink:        #4A2C93;  /* brand-coloured text on a light ground, AA on --brand-soft */

  /* Ground and surfaces. Warm, same paper as the marketing site. */
  --bg:               #FBF8F3;  /* page */
  --surface:          #FFFFFF;  /* cards, table bodies, inputs */
  --surface-2:        #F3EEE5;  /* sidebar, table headers, inset panels */
  --surface-3:        #EFE7D9;  /* the warmest step, used sparingly */

  /* Text. Contrast ratios are against --bg. */
  --ink:              #241C38;  /* headings. 11.7:1 */
  --ink-body:         #3C3452;  /* body copy. 11.0:1 */
  --ink-muted:        #6C6380;  /* labels, captions, secondary cells. 5.3:1, passes AA */
  --ink-dim:          #8B8399;  /* disabled text and decorative marks ONLY. 3.5:1, fails AA
                                   for body copy, so never use it for anything a user has
                                   to read. */
  --ink-invert:       #FBF8F3;  /* text on a --brand fill. 8.6:1 */

  /* Lines. Transparent purple so a hairline tints whatever surface it sits on,
     exactly as on the marketing site. */
  --line:             rgba(85, 52, 165, 0.14);
  --line-strong:      rgba(85, 52, 165, 0.32);

  /* Semantic set. Kept from the old portal because the marketing site has no
     equivalent and a work tool genuinely needs one, but retuned to sit on
     cream rather than lavender. Every ink below passes AA on its own -bg. */
  --ok:               #2E6B47;
  --ok-bg:            #E8F1EA;
  --warn:             #8A5A12;
  --warn-bg:          #FAF0DE;
  --bad:              #9E2F22;
  --bad-bg:           #FBEAE7;
  --neutral:          #5B5470;
  --neutral-bg:       #F1EEF6;

  /* Type */
  --font-display:     'Source Serif 4', Georgia, 'Times New Roman', serif;
  --font-ui:          'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono:        'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Radius.
     The marketing site is 3px everywhere. 3px is right on a 40px-tall marketing
     button but reads as an unfinished square once you have 44px inputs, table
     cards and modals stacked together, and Notion sits at 4 to 6px. 5px is the
     midpoint: still visibly square-shouldered at card size, still deliberate at
     button size. One value, used for cards, inputs, buttons, modals, drawers
     and calendar cells. --radius-sm is only for inline tags, to match the 2px
     tags on the marketing site. --radius-pill is only for status pills. */
  /* Softer than the marketing site's 3px on purpose. Print-sharp corners suit
     an editorial page you read once; a tool someone works in for an hour reads
     as brittle at that radius, and every card, input and button here carries
     one. */
  --radius:           10px;   /* inputs, modals, drawers, calendar cells */
  --radius-sm:        6px;    /* inline tags, checkbox squares */
  --radius-pill:      999px;  /* every button and chip */
  /* Design 4: cards are 14px and buttons are fully round. The two radii doing
     different jobs is what stops a page of eight cards reading as a wireframe
     -- the card is a surface, the button is an object sitting on it. */
  --radius-card:      14px;
  --radius-row:       9px;    /* the tinted current row inside a card */

  /* Spacing scale. Use these, do not invent new gaps. --gap is THE gutter:
     every grid in the portal uses it, the way the marketing site uses 14px
     everywhere. */
  --space-1:          4px;
  --space-2:          8px;
  --space-3:          12px;
  --space-4:          16px;
  --space-5:          20px;
  --space-6:          24px;
  --space-8:          32px;
  --space-10:         40px;
  --gap:              16px;
  --card-pad:         20px;

  /* Layout */
  --sidebar-w:        236px;
  --topbar-h:         56px;
  --content-max:      1240px;  /* same cap as the marketing site */
  /* The bar and the page share this. Set independently they drift apart at
     whatever width nobody checked, which is exactly what happened. */
  --gutter:           44px;

  /* The only two shadows in the file. */
  --shadow-btn:       0 6px 20px rgba(85, 52, 165, 0.22);
  --shadow-overlay:   0 24px 60px -20px rgba(36, 28, 56, 0.30);

  /* Motion. Short and boring on purpose. Colour only, never transform. */
  --t:                120ms ease;
}


/* ==========================================================================
   2. NO DARK MODE, ON PURPOSE
   The marketing site is light only. When this file carried a
   prefers-color-scheme block, the portal flipped to a dark ground on any
   device set to dark while iled.uk stayed cream, so signing in looked like
   arriving at a different company.

   The palette in section 1 is the marketing palette, so the portal now
   matches it everywhere. If a dark portal is ever wanted, it needs the
   marketing site to gain one at the same time.
   ========================================================================== */


/* ==========================================================================
   3. BASE
   Scoped to .ile-portal so this file can load alongside ile.css on the login
   page without touching the marketing nav or footer.
   ========================================================================== */

.ile-portal,
.ile-portal *,
.ile-portal *::before,
.ile-portal *::after { box-sizing: border-box; }

.ile-portal {
  margin: 0;
  background: var(--bg);
  color: var(--ink-body);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* Belt and braces against a sideways scrollbar. The real fix is min-width:0
     on the grid children, which is applied on .ile-main below. */
  overflow-x: hidden;
}

/* No weight here on purpose. This rule is (0,1,1) and every component title
   selector is (0,1,0), so setting a weight here quietly beat all of them --
   which is exactly what happened to the 400-weight display headings until it
   was caught by reading computed styles rather than looking at the page. */
.ile-portal h1,
.ile-portal h2,
.ile-portal h3,
.ile-portal h4 { margin: 0; color: var(--ink); line-height: 1.25; }

.ile-portal p { margin: 0 0 var(--space-3); }
.ile-portal p:last-child { margin-bottom: 0; }

/* Cormorant ships oldstyle figures by default: the 1 is a short stem that
   reads as a Roman numeral, and 3 4 5 7 9 hang below the baseline. A stat tile
   showing "11" comes out looking like "II". Every place the display face can
   contain a number needs lining figures turned on. Checked in a browser, not
   assumed. */
.ile-brand,
.ile-auth-brand,
.ile-page-title,
.ile-section-title,
.ile-card-title,
.ile-modal-title,
.ile-drawer-title,
.ile-cal-title,
.ile-auth-title,
.ile-empty-title { font-variant-numeric: lining-nums; }

.ile-portal a { color: var(--brand-ink); text-decoration: none; }
.ile-portal a:hover { text-decoration: underline; }

.ile-portal ::selection { background: rgba(85, 52, 165, 0.22); color: var(--ink); }

/* Every interactive thing gets the same ring, copied from the marketing site.
   The old portal file had no focus style at all, which was an accessibility
   regression from the public pages. */
.ile-portal :focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  .ile-portal *,
  .ile-portal *::before,
  .ile-portal *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}


/* ==========================================================================
   4. APP SHELL
   Fixed sidebar, content column, sticky top bar. Below 900px the sidebar
   becomes an off-canvas panel driven by .is-open, with .ile-scrim behind it.

   <body class="ile-portal">
     <div class="ile-app">
       <aside class="ile-sidebar" id="sidebar"> ... </aside>
       <div class="ile-scrim" id="scrim"></div>
       <div class="ile-main">
         <header class="ile-topbar"> ... </header>
         <main class="ile-content"> ... </main>
       </div>
     </div>
   ========================================================================== */

.ile-app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* ---- Top navigation ----
   Was a 236px sidebar. Each role has five destinations, so that column spent
   most of its width empty on every screen while the tables and the marking
   view fought for room. A top bar gives the content the full width, matches
   the marketing site so signing in does not feel like a different product,
   and collapses to one menu on a phone instead of a drawer sliding over the
   page. */
.ile-topnav {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  height: var(--topbar-h);
  /* The bar stays full width so its background and rule reach the edges, but
     its contents follow the same 1240 cap the page uses. Without the max(),
     on a wide monitor the wordmark sat at the window edge while the page title
     began 130px inboard. */
  padding: 0 max(var(--gutter), calc((100% - var(--content-max)) / 2 + var(--gutter)));
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

/* Same wordmark as the marketing nav and the sign-in screen: sans, tracked
   wide. Lost when the sidebar block was replaced, which left it inheriting
   14px body text and no tracking. */
.ile-brand {
  display: inline-block;
  font-family: var(--font-ui);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.26em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}
.ile-brand:hover { text-decoration: none; }

/* Count beside a nav label, e.g. how many papers are waiting. */
.ile-nav-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--brand-soft);
  color: var(--brand-ink);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.ile-nav-item.is-active .ile-nav-count { background: var(--brand); color: var(--ink-invert); }
/* The shell writes the span whether or not there is a number in it, so an
   empty one would otherwise sit next to every label as a blank pill. */
.ile-nav-count:empty { display: none; }

.ile-topnav-end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* The role sits with the wordmark rather than under it, so the two read as
   one lockup: "ILE · Tutor". A tinted chip rather than a ruled-off caption:
   it is the one thing in the bar that differs between the four portals, so it
   should be the one thing in the bar with a fill. */
.ile-role {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--brand-ink);
  background: var(--brand-soft);
  border-radius: var(--radius-pill);
  padding: 4px 10px;
  white-space: nowrap;
}

.ile-content {
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 42px var(--gutter) 50px;
  flex: 1;
}

/* ---- Nav items ----
   Uppercase and tracked, marked by a 2px rule rather than a filled pill. A
   filled pill in the bar competes with the one filled brand surface the page
   is allowed (the next-lesson block), and two purple fills on a screen means
   neither one is the answer. Every item carries the transparent border so
   becoming active does not move the row. */
.ile-nav {
  display: flex;
  align-items: center;
  gap: 24px;
  min-width: 0;
}
.ile-nav-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 0;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: transparent;
  color: var(--ink-muted);
  font-family: var(--font-ui);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--t), border-color var(--t);
}
.ile-nav-item:hover { color: var(--ink); text-decoration: none; }
.ile-nav-item.is-active { color: var(--brand-ink); border-bottom-color: var(--brand); }
.ile-nav-icon { width: 16px; height: 16px; flex-shrink: 0; }

/* Hamburger, shown once the bar runs out of room. */
.ile-menu-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-left: auto;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--t), border-color var(--t);
}
.ile-menu-btn:hover { background: var(--brand-tint); border-color: var(--line-strong); }

.ile-scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(36, 28, 56, 0.40);
}
.ile-scrim.is-open { display: block; }

/* ---- Shell responsive ----
   Below 900 the nav drops out of the bar and becomes a panel under it. It
   keeps the id the shell script toggles, so nothing in JS changes. */
@media (max-width: 900px) {
  :root { --gutter: 20px; }
  .ile-topnav { gap: var(--space-3); }

  .ile-nav {
    display: none;
    position: fixed;
    top: var(--topbar-h);
    left: 0;
    right: 0;
    z-index: 45;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: var(--space-3);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-overlay);
  }
  .ile-nav.is-open { display: flex; }
  /* In the panel the items are a list again, so they go back to sentence case
     at a tappable size -- 11.5px uppercase is a bar treatment, not a menu. */
  .ile-nav-item {
    padding: 13px var(--space-3);
    font-size: 15px;
    letter-spacing: 0;
    text-transform: none;
    border-bottom: 0;
    border-radius: var(--radius);
    min-height: 44px;
  }
  .ile-nav-item.is-active { background: var(--brand-soft); }

  .ile-menu-btn { display: inline-flex; }
  .ile-topnav-end { margin-left: auto; }
  .ile-identity-name { display: none; }   /* avatar only, to save the row */
  .ile-content { padding: 24px var(--gutter) 30px; }
}


/* Between 900 and 1120 the labels are what overflow first, so the bar tightens
   before anything is dropped. */
@media (min-width: 901px) and (max-width: 1200px) {
  :root { --gutter: 28px; }
  .ile-topnav { gap: var(--space-4); }
  .ile-nav { gap: 18px; }
  .ile-nav-item { font-size: 11px; }
}


/* ==========================================================================
   5. PAGE HEADER
   Title, optional one-line subtitle, optional actions on the right. The
   italic-purple word inside a heading is a marketing signature, so
   <h1 class="ile-page-title">Your <em>lessons</em></h1> is encouraged.
   ========================================================================== */

.ile-page-head {
  display: flex;
  align-items: flex-end;
  gap: var(--gap);
  flex-wrap: wrap;
  margin-bottom: var(--space-8);
}

/* The date, or who the page is about. Mono and tracked so it reads as a label
   on the page rather than as the first line of it. */
.ile-page-eyebrow {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 12px;
}

/* 42px at regular weight. Source Serif at 400 is already darker on the page
   than the UI face at 500, so bolding it only makes it shout. */
.ile-page-title {
  font-family: var(--font-display);
  font-size: 42px;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.12;
  color: var(--ink);
}
.ile-page-title em { font-style: italic; color: var(--brand-ink); }

/* This is the sentence that answers what the user came for, so it is a real
   subhead rather than a caption: body colour, generous leading, and a measure
   short enough to read in one go. Write it from the data, never statically. */
.ile-page-sub {
  margin-top: 10px;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--ink-body);
  max-width: 60ch;
}
.ile-page-actions { margin-left: auto; display: flex; gap: var(--space-2); flex-wrap: wrap; }

@media (max-width: 640px) {
  .ile-page-head { margin-bottom: var(--space-6); }
  .ile-page-title { font-size: 31px; }
  .ile-page-sub { font-size: 14px; }
  /* On a phone the page actions are in the sticky bar at the bottom instead,
     where a thumb can reach them. */
  .ile-page-actions { display: none; }
}

/* ---- Breadcrumb ---- */
.ile-breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  font-size: 12px;
  color: var(--ink-muted);
}
.ile-breadcrumb a { color: var(--ink-muted); }
.ile-breadcrumb a:hover { color: var(--brand-ink); }
.ile-breadcrumb-sep { color: var(--ink-dim); }
.ile-breadcrumb [aria-current="page"] { color: var(--ink); font-weight: 500; }


/* ==========================================================================
   6. SECTION HEADER
   The marketing pattern: a small uppercase brand label above a display
   heading. Replaces the 4px gradient bar from the old portal.
   ========================================================================== */

.ile-section { margin-bottom: var(--space-8); }

/* The title, an optional intro and optional actions are all siblings here.
   As a flex row the intro sat on the same line as its own heading, which is
   why "Waiting on your notes" read as broken while every heading without an
   intro looked fine. Wrapping did not fix it either: the intro's max-width
   clamps its flex base size, so it always fitted beside the title and never
   wrapped. Grid places it on its own row explicitly and keeps the reading
   measure. */
.ile-section-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 4px var(--gap);
  margin-bottom: var(--space-4);
}
.ile-section-head > .ile-section-title { grid-column: 1; }
.ile-section-head > .ile-section-intro { grid-column: 1 / -1; }
.ile-section-head > .ile-section-actions { grid-column: 2; grid-row: 1; margin-left: 0; }
.ile-section-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-ink);
  margin-bottom: 6px;
}
.ile-section-title {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 400;
  color: var(--ink);
}
.ile-section-intro {
  margin-top: 2px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-muted);
  max-width: 62ch;
}
.ile-section-actions { margin-left: auto; display: flex; gap: var(--space-2); align-items: center; }

/* The count that sits opposite a section heading -- "Three", "Six lessons",
   "Two outstanding". Words, not digits: a numeral here would be the third
   number on the row and would read as data rather than as a label. */
.ile-section-count {
  grid-column: 2;
  grid-row: 1;
  align-self: end;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
}
.ile-section-head > .ile-section-count { margin-left: auto; }

@media (max-width: 640px) {
  .ile-section-title { font-size: 20px; }
}


/* ==========================================================================
   7. CARD
   A 1px hairline on a white surface. No shadow, no hover lift, ever. That one
   decision is most of what makes this look like the website again.
   ========================================================================== */

/* A hairline plus a very soft drop is calmer than a hard outline alone: on a
   page of eight cards, pure outlines read as a wireframe rather than a
   finished surface. The shadow is barely there, just enough to lift the card
   off the ground. */
.ile-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: var(--card-pad);
}
/* Use on a card that contains a table or a list that draws its own dividers,
   so the rows can run edge to edge. */
.ile-card.is-flush { padding: 0; overflow: hidden; }
/* A quieter card for panels that carry a message rather than data: the office
   note to parents, the unbilled summary, the papers-to-mark prompt. Tinted
   rather than white so it reads as commentary on the page, not another table.
   Despite the name it does not go inside another card -- cards never nest. */
.ile-card.is-inset,
.ile-card.is-tinted { background: var(--surface-2); }

/* The heading on a rail card. Mono and tracked, sitting directly on the card
   rather than on a ruled header row, because a rail card holds four rows and
   a ruled header would be a third of its height. */
.ile-card-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-ink);
  margin-bottom: var(--space-3);
}
.ile-card.is-tinted .ile-card-label { color: var(--ink-muted); }

.ile-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}
.ile-card.is-flush .ile-card-head {
  padding: var(--space-4) var(--card-pad);
  margin-bottom: 0;
}
.ile-card-title {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.ile-card-actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
.ile-card-foot {
  padding-top: var(--space-3);
  margin-top: var(--space-4);
  border-top: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ile-card.is-flush .ile-card-foot { padding: var(--space-3) var(--card-pad); margin-top: 0; }


/* ==========================================================================
   8. GRID
   One gutter value everywhere. Columns collapse at 900px and again at 620px.
   ========================================================================== */

.ile-grid { display: grid; gap: var(--gap); }
.ile-grid.is-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ile-grid.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ile-grid.is-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Main column plus a narrower side column, for a detail page. */
.ile-grid.is-split { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }

@media (max-width: 900px) {
  .ile-grid.is-3,
  .ile-grid.is-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ile-grid.is-split { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 620px) {
  .ile-grid.is-2,
  .ile-grid.is-3,
  .ile-grid.is-4 { grid-template-columns: minmax(0, 1fr); }
}


/* ==========================================================================
   9. STAT TILE
   Cormorant for the figure, because a large serif number is the one place the
   brand voice survives inside a work tool.
   ========================================================================== */

.ile-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  margin-bottom: var(--space-8);
}
.ile-stats.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Two by two on a phone rather than one column: four stacked cards push the
   thing the page is actually about below the fold, and these are context, not
   the answer. */
@media (max-width: 640px) {
  .ile-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .ile-stats.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.ile-stat {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 20px 22px;
}

/* At most one of these per screen. A red card among four white ones is read
   before anything else on the page, which is only useful while it is rare. */
.ile-stat.is-bad { border-color: rgba(158, 47, 34, 0.22); }
.ile-stat.is-bad .ile-stat-value,
.ile-stat.is-bad .ile-stat-meta { color: var(--bad); }

.ile-stat-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ile-stat-value {
  margin-top: 8px;
  font-family: var(--font-display);
  font-size: 31px;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink);
  /* lining-nums is what stops "11" reading as "II". See the note in BASE. */
  font-variant-numeric: lining-nums tabular-nums;
}
/* Use on a stat whose value is money or a percentage. Mono keeps the decimal
   points of a row of tiles in line with each other. */
.ile-stat-value.is-mono { font-family: var(--font-mono); font-size: 26px; font-weight: 500; }

/* A sentence, not a delta. "Four more than last week" tells someone what the
   number means; "+4" makes them work it out, and half the time they work it
   out wrong. */
.ile-stat-meta { margin-top: 6px; font-size: 12px; line-height: 1.5; color: var(--ink-muted); }

@media (max-width: 640px) {
  /* "Hours this week" wraps to two lines and "Notes owed" does not, so the
     figures sat at different heights across the row and the three cards read
     as three unrelated boxes. The value is pushed to the bottom of the card
     instead, which lines them up whatever the label does. */
  .ile-stat { padding: 14px 15px; display: flex; flex-direction: column; }
  .ile-stat-value { margin-top: auto; padding-top: 8px; }
  .ile-stat-label { font-size: 9px; letter-spacing: 0.12em; }
  .ile-stat-value { font-size: 24px; }
  .ile-stats.is-3 .ile-stat-value { font-size: 21px; }
  .ile-stat-value.is-mono { font-size: 19px; }
  /* The meta sentence is the first thing to go: it is the explanation, and on
     a phone the figure with its label is enough. */
  .ile-stat-meta { display: none; }
}
/* Direction is stated in the text as well as the colour, e.g. "up 12%". */
.ile-stat-delta.is-up { color: var(--ok); }
.ile-stat-delta.is-down { color: var(--bad); }


/* ==========================================================================
   10. TABLE
   Hairline rows, no zebra striping. Numeric columns are right-aligned and set
   in mono with tabular figures so digits stack.

   ALWAYS wrap a table in .ile-table-wrap. That is the documented horizontal
   scroll container, and it is what stops a wide table from making the whole
   page scroll sideways on a phone. Give the wrap a max-height as well if you
   want the sticky header to actually stick; without one it sticks to the page
   scroll root, which is usually fine but behaves differently.
   ========================================================================== */

.ile-table-wrap {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.ile-table {
  width: 100%;
  min-width: 640px;              /* forces the wrap to scroll instead of squashing */
  border-collapse: collapse;
  font-size: 14px;
}
/* No fill behind the header row. The band was doing the job the hairline does,
   and two devices for one job is what made the old tables read as heavy. */
.ile-table th {
  background: transparent;
  text-align: left;
  padding: 10px var(--space-3);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
.ile-table td {
  padding: 13px var(--space-3);
  border-bottom: 1px solid var(--line);
  color: var(--ink-body);
  vertical-align: middle;
}
.ile-table tbody tr:last-child td { border-bottom: 0; }
.ile-table tr.is-clickable { cursor: pointer; }
.ile-table tr.is-clickable:hover td { background: var(--brand-tint); }

/* ---- Reference-first tables ----
   OPT-IN, not the default. When the first column is a time or a reference it
   should be mono, muted and narrow: it is how you find your row, not what you
   read once you have found it.

   This is a class rather than a bare td:first-child rule because most tables
   in this portal lead with a name -- People, Billing, Marking, Papers -- and
   setting every first cell in mono at width:1% would have squashed a column
   of names on six screens to fix two. */
.ile-table.is-ref-first td:first-child {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  width: 1%;
}
/* The "what" column carries the weight; the "who" column is quieter. */
.ile-table td.is-what { color: var(--ink); }
.ile-table td.is-who { color: var(--ink-body); }
.ile-table td.is-status,
.ile-table th.is-status { text-align: right; white-space: nowrap; }

/* ---- The current row ----
   One per table. Tinted and rounded rather than ruled: the tint says "this one"
   without adding a fifth line to a page already full of hairlines. The row
   drops its own rules so the tint reads as a single shape. */
.ile-table tr.is-current td,
.ile-table tr.is-selected td {
  background: var(--brand-soft);
  border-bottom-color: transparent;
}
.ile-table tr.is-current + tr td { border-top: 0; }
.ile-table tr.is-current td:first-child {
  border-top-left-radius: var(--radius-row);
  border-bottom-left-radius: var(--radius-row);
  color: var(--brand-ink);
  font-weight: 500;
}
.ile-table tr.is-current td:last-child {
  border-top-right-radius: var(--radius-row);
  border-bottom-right-radius: var(--radius-row);
}
.ile-table tr.is-current td.is-what { color: var(--brand-ink); font-weight: 500; }

/* Right-aligned mono column. Put it on both the th and the td. */
.ile-table .is-num,
.ile-table th.is-num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.ile-table th.is-num { font-family: var(--font-ui); }
.ile-table .is-actions { text-align: right; white-space: nowrap; width: 1%; }
.ile-table caption {
  caption-side: top;
  text-align: left;
  padding: 0 var(--space-5) var(--space-3);
  font-size: 12px;
  color: var(--ink-muted);
}

/* A full-width message row inside the tbody when there is nothing to show. */
.ile-table-empty td {
  padding: var(--space-8) var(--space-5);
  text-align: center;
  color: var(--ink-muted);
}


/* ==========================================================================
   11. DETAIL LIST
   Label and value pairs on a record page. Use a real <dl>.
   ========================================================================== */

.ile-dl { margin: 0; }
.ile-dl-row {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  gap: var(--gap);
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.ile-dl-row:last-child { border-bottom: 0; }
.ile-dl-term {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding-top: 2px;
}
.ile-dl-value { margin: 0; color: var(--ink-body); }
.ile-dl.is-stacked .ile-dl-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }

@media (max-width: 620px) {
  .ile-dl-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}


/* ==========================================================================
   12. STATUS PILL
   ACCESSIBILITY RULE: colour is never the only signal. Every pill contains its
   own text label, and the dot is decorative. Do not build a bare coloured dot
   with no words next to it.

   Class names follow the database values exactly, so you can do
   pill.className = 'ile-pill is-' + row.status.replace('_','-')

   lessons      scheduled | completed | cancelled | no-show
   invoices     draft | sent | paid | overdue | void
   submissions  queued | marking | marked | needs-review | failed
   payouts      pending | paid
   ========================================================================== */

.ile-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  background: var(--neutral-bg);
  color: var(--neutral);
}
.ile-pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
/* Hides the dot where space is tight, e.g. inside a calendar chip. */
.ile-pill.is-bare::before { display: none; }

/* Green: the good end state. */
.ile-pill.is-completed,
.ile-pill.is-paid,
.ile-pill.is-marked,
.ile-pill.is-active,
.ile-pill.is-confirmed { background: var(--ok-bg); color: var(--ok); }

/* Amber: waiting on someone, or needs a human. */
.ile-pill.is-pending,
.ile-pill.is-sent,
.ile-pill.is-queued,
.ile-pill.is-marking,
.ile-pill.is-needs-review { background: var(--warn-bg); color: var(--warn); }

/* Red: went wrong or is late. */
.ile-pill.is-overdue,
.ile-pill.is-cancelled,
.ile-pill.is-failed,
.ile-pill.is-no-show { background: var(--bad-bg); color: var(--bad); }

/* Purple: live and on the books, nothing wrong. Scheduled is the commonest
   status in the whole product, so it gets the brand rather than a warning
   colour. */
.ile-pill.is-scheduled { background: var(--brand-soft); color: var(--brand-ink); }

/* Grey: not started, or deliberately parked. */
.ile-pill.is-draft,
.ile-pill.is-void,
.ile-pill.is-paused,
.ile-pill.is-neutral { background: var(--neutral-bg); color: var(--neutral); }

/* Outlined variant for a pill sitting on an already-tinted surface. */
.ile-pill.is-outline { background: transparent; border-color: var(--line-strong); color: var(--ink-muted); }


/* ==========================================================================
   13. BADGE AND TAG
   Badge is a count. Tag is a label such as a subject or an exam board.
   ========================================================================== */

.ile-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--ink-invert);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
}
.ile-badge.is-quiet { background: var(--neutral-bg); color: var(--neutral); }

.ile-tag {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
/* A provisional or unconfirmed tag sitting next to a normal one - e.g. an AI
   mark nobody has checked yet, beside one the office has already corrected.
   Text stays --ink-muted (5.3:1, passes AA) - never --ink-dim, which fails AA
   and is reserved for decorative marks only (see the token comment in
   section 1). The dashed, backgroundless border is what actually reads as
   quieter. */
.ile-tag.is-quiet { background: transparent; border-style: dashed; }


/* ==========================================================================
   14. AVATAR AND IDENTITY
   Initials only. No image uploads anywhere in this product.
   ========================================================================== */

.ile-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand-ink);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  user-select: none;
}
.ile-avatar.is-sm { width: 26px; height: 26px; font-size: 10px; }
.ile-avatar.is-lg { width: 44px; height: 44px; font-size: 15px; }
.ile-avatar.is-solid { background: var(--brand); color: var(--ink-invert); }

/* Avatar plus a name and a line of meta. The standard first cell of a table. */
.ile-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ile-identity-name { font-weight: 500; color: var(--ink); line-height: 1.3; }
.ile-identity-meta { font-size: 12px; color: var(--ink-muted); line-height: 1.3; }


/* ==========================================================================
   15. BUTTONS
   Flat fills only. The primary button is the single element in the portal
   allowed a shadow, matching .btn-primary on the marketing site. Secondary and
   ghost tint on hover. Nothing translates on hover.
   ========================================================================== */

.ile-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 9px 20px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: transparent;
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t), border-color var(--t), color var(--t);
}
.ile-btn:hover { text-decoration: none; }

/* Flat. The shadow was the last thing in the portal pretending to be a
   different material from the page it sits on. */
.ile-btn.is-primary {
  background: var(--brand);
  color: var(--ink-invert);
}
.ile-btn.is-primary:hover { background: var(--brand-hover); }

.ile-btn.is-secondary {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink-body);
}
.ile-btn.is-secondary:hover { background: var(--brand-tint); border-color: var(--brand); color: var(--ink); }

.ile-btn.is-ghost { color: var(--ink-muted); }
.ile-btn.is-ghost:hover { background: var(--brand-tint); color: var(--ink); }

.ile-btn.is-danger { border-color: rgba(158, 47, 34, 0.35); color: var(--bad); }
.ile-btn.is-danger:hover { background: var(--bad-bg); border-color: var(--bad); }

.ile-btn.is-sm { padding: 6px 14px; font-size: 12.5px; }
.ile-btn.is-lg { padding: 13px 28px; font-size: 15px; }
.ile-btn.is-block { width: 100%; }

/* The action on a list row. An outlined pill rather than a text link: it sits
   at the end of a row of running text, and an underlined link there reads as
   part of the sentence. */
.ile-btn.is-row {
  padding: 8px 16px;
  font-size: 12.5px;
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink-body);
}
.ile-btn.is-row:hover { background: var(--brand-tint); border-color: var(--brand); color: var(--brand-ink); }

/* Buttons sitting on the brand block: cream fill for the one real action,
   outlined-on-purple for the second. */
.ile-btn.is-on-brand {
  background: var(--ink-invert);
  color: var(--brand-ink);
  border-color: transparent;
}
.ile-btn.is-on-brand:hover { background: #FFFFFF; }
.ile-btn.is-on-brand-ghost {
  background: transparent;
  color: #E5DBF7;
  border-color: rgba(251, 248, 243, 0.40);
}
.ile-btn.is-on-brand-ghost:hover { background: rgba(251, 248, 243, 0.12); color: var(--ink-invert); }

.ile-btn:disabled,
.ile-btn[aria-disabled="true"],
.ile-btn.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
  box-shadow: none;
  pointer-events: none;
}

/* Loading: set aria-busy="true" and disable it. The label stays put and a
   spinner appears in front of it, so the button does not change width. */
.ile-btn.is-loading { pointer-events: none; position: relative; color: transparent; }
.ile-btn.is-loading::after {
  content: '';
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--ink-invert);
  animation: ile-spin 700ms linear infinite;
}
.ile-btn.is-loading.is-secondary::after,
.ile-btn.is-loading.is-ghost::after { color: var(--ink-muted); }
@keyframes ile-spin { to { transform: rotate(360deg); } }

/* Square icon-only button. Always give it an aria-label. */
.ile-btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-muted);
  cursor: pointer;
  transition: background var(--t), color var(--t), border-color var(--t);
}
.ile-btn-icon:hover { background: var(--brand-tint); color: var(--ink); border-color: var(--line-strong); }

.ile-btn-group { display: inline-flex; gap: var(--space-2); flex-wrap: wrap; }


/* ==========================================================================
   16. FORMS
   Label above input, hint below, error replaces the hint. Never rely on the
   red border alone: always render the .ile-error text and point the input at
   it with aria-describedby.
   ========================================================================== */

.ile-form { display: grid; gap: var(--space-5); }
.ile-form-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap); }
.ile-form-row.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* A row of buttons at the bottom of a form. Wraps rather than squeezing,
   because "Publish to the family" is a long label on purpose and truncating
   it would hide what the button actually does. */
.ile-form-actions { display: flex; flex-wrap: wrap; gap: var(--gap); align-items: center; }

/* "Not an hour, or not online". A form that asks a question whose answer is
   always the same trains people to stop reading it, so the three constants
   live behind this instead of in front of every booking. */
.ile-disclosure { border-top: 1px solid var(--line); padding-top: var(--space-4); }
.ile-disclosure > summary {
  cursor: pointer; list-style: none;
  font-size: 12.5px; color: var(--ink-muted);
  display: inline-flex; align-items: center; gap: 8px;
}
.ile-disclosure > summary::-webkit-details-marker { display: none; }
.ile-disclosure > summary::before {
  content: "+"; font-family: var(--font-mono); font-size: 13px; line-height: 1;
  color: var(--brand);
}
.ile-disclosure[open] > summary::before { content: "\2212"; }
.ile-disclosure > summary:hover { color: var(--ink); }
.ile-disclosure > div { margin-top: var(--space-4); }

/* One question on the record-a-paper grid. Not .ile-row: that is a list row
   with a fixed 30px / 1fr / auto shape, and dropping six form fields into it
   collapses them into the index column. This is its own thing. */
.ile-qrow {
  display: grid;
  grid-template-columns: 92px 132px 96px 96px minmax(0, 1fr) auto;
  gap: 8px 14px;
  align-items: end;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.ile-qrow:last-child { border-bottom: 0; }
.ile-qrow .ile-label { font-size: 10.5px; margin-bottom: 4px; }
.ile-qrow-actions { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
/* The arithmetic check sits next to Remove and must not push it off the row. */
.ile-qrow-check { font-size: 11.5px; color: var(--ink-muted); }
.ile-qrow-check.is-off { color: var(--warn); }

@media (max-width: 900px) {
  .ile-qrow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ile-qrow-actions { grid-column: 1 / -1; justify-content: space-between; }
}
@media (max-width: 620px) {
  .ile-form-row,
  .ile-form-row.is-3 { grid-template-columns: minmax(0, 1fr); }
}

.ile-field { display: flex; flex-direction: column; gap: 6px; }
.ile-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ile-required { color: var(--bad); margin-left: 2px; }

.ile-input,
.ile-select,
.ile-textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-body);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.5;
  transition: border-color var(--t), background var(--t);
}
.ile-input::placeholder,
.ile-textarea::placeholder { color: var(--ink-dim); }
.ile-input:hover,
.ile-select:hover,
.ile-textarea:hover { border-color: var(--brand); }
.ile-input:focus,
.ile-select:focus,
.ile-textarea:focus { border-color: var(--brand); outline: 2px solid var(--brand); outline-offset: 1px; }
.ile-input:disabled,
.ile-select:disabled,
.ile-textarea:disabled { background: var(--surface-2); color: var(--ink-dim); cursor: not-allowed; }

.ile-textarea { min-height: 96px; resize: vertical; }

/* Native select with a chevron drawn as an inline SVG data URI. data: is
   allowed for images by the CSP, and this is a background-image, so it is
   fine. */
.ile-select {
  appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%236C6380' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

/* The chevron above is a data-URI SVG, and a CSS custom property cannot be
   interpolated into a url() string, so the --ink-muted swap in section 2
   never reaches it. Same arrow, --ink-muted's dark value (#A9A2BA) baked in
   instead, so the dropdown arrow matches every other muted icon in dark
   mode rather than staying tuned for light. */

/* Amounts and reference numbers are typed in mono so they match how they are
   displayed everywhere else. */
.ile-input.is-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Prefixed input, for a currency symbol. */
.ile-input-group { display: flex; align-items: stretch; }
.ile-input-affix {
  display: flex;
  align-items: center;
  padding: 0 10px;
  border: 1px solid var(--line-strong);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--surface-2);
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: 13px;
}
.ile-input-group .ile-input { border-radius: 0 var(--radius) var(--radius) 0; }

.ile-hint { font-size: 12px; color: var(--ink-muted); }
.ile-error { font-size: 12px; font-weight: 500; color: var(--bad); }
.ile-field.has-error .ile-input,
.ile-field.has-error .ile-select,
.ile-field.has-error .ile-textarea { border-color: var(--bad); }

/* Checkbox and radio. The native control is kept and just recoloured, which is
   far more reliable than a custom one and keeps keyboard behaviour for free. */
.ile-check { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.ile-check input[type="checkbox"],
.ile-check input[type="radio"] {
  width: 16px;
  height: 16px;
  margin: 2px 0 0;
  accent-color: var(--brand);
  flex-shrink: 0;
  cursor: pointer;
}
.ile-check-label { font-size: 14px; color: var(--ink-body); line-height: 1.5; }
.ile-check-hint { display: block; font-size: 12px; color: var(--ink-muted); }

.ile-fieldset { border: 0; padding: 0; margin: 0; display: grid; gap: var(--space-3); }
.ile-legend {
  padding: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}


/* ==========================================================================
   17. FILE DROPZONE
   For student mock-paper uploads. Wrap a real <input type="file"> and keep it
   in the DOM, visually hidden, so keyboard and screen readers still work.
   Add .is-dragover on dragenter, remove it on dragleave and drop.
   ========================================================================== */

.ile-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: var(--space-8) var(--space-5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  text-align: center;
  cursor: pointer;
  transition: background var(--t), border-color var(--t);
}
.ile-dropzone:hover { background: var(--brand-tint); border-color: var(--brand); }
.ile-dropzone.is-dragover { background: var(--brand-soft); border-color: var(--brand); border-style: solid; }
.ile-dropzone.has-error { border-color: var(--bad); background: var(--bad-bg); }
.ile-dropzone-icon { width: 24px; height: 24px; color: var(--ink-muted); }
.ile-dropzone-text { font-size: 14px; font-weight: 500; color: var(--ink); }
.ile-dropzone-hint { font-size: 12px; color: var(--ink-muted); }
.ile-dropzone input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The chosen file, listed under the dropzone. */
.ile-file {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  margin-top: var(--space-2);
}
.ile-file-name { font-size: 13px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ile-file-size { font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); margin-left: auto; }
.ile-file-remove { border: 0; background: none; color: var(--ink-muted); cursor: pointer; padding: 2px; }
.ile-file-remove:hover { color: var(--bad); }

/* Thin determinate progress bar for the upload. Set width from JS with
   el.style.width, which is a CSSOM write and is not affected by the CSP. */
.ile-progress { height: 3px; border-radius: var(--radius-pill); background: var(--surface-3); overflow: hidden; margin-top: var(--space-2); }
.ile-progress-bar { height: 100%; width: 0; background: var(--brand); transition: width 200ms linear; }


/* ==========================================================================
   18. MODAL
   Overlays are the one in-flow exception to the no-shadow rule: a shadow is
   what tells you the thing is above the page rather than part of it.

   Prefer a native <dialog> if you can. If not, use role="dialog"
   aria-modal="true", move focus into it on open, and restore it on close.
   ========================================================================== */

.ile-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(36, 28, 56, 0.40);
  padding: var(--space-5);
  overflow-y: auto;
}
.ile-modal-backdrop.is-open { display: flex; align-items: flex-start; justify-content: center; }

.ile-modal {
  width: 100%;
  max-width: 560px;
  margin: 8vh auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-overlay);
}
.ile-modal.is-wide { max-width: 860px; }

.ile-modal-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
}
.ile-modal-title { font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink); }
.ile-modal-close {
  margin-left: auto;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--ink-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: background var(--t), color var(--t);
}
.ile-modal-close:hover { background: var(--brand-tint); color: var(--ink); }

.ile-modal-body { padding: var(--space-5); }
.ile-modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: 0 0 var(--radius) var(--radius);
}


/* ==========================================================================
   19. DRAWER
   Right-hand panel for a record you want to read without losing the list
   behind it. Same backdrop element as the modal.
   ========================================================================== */

.ile-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 61;
  width: min(480px, 100%);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-overlay);
  transform: translateX(100%);
  transition: transform 180ms ease;
}
.ile-drawer.is-open { transform: translateX(0); }
.ile-drawer.is-wide { width: min(720px, 100%); }

.ile-drawer-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--line);
}
.ile-drawer-title { font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink); }
.ile-drawer-body { flex: 1; overflow-y: auto; padding: var(--space-5); }
.ile-drawer-foot {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}


/* ==========================================================================
   20. TABS
   Use real buttons with role="tab", aria-selected and aria-controls. The panel
   is hidden with the hidden attribute, not with a class, so it is hidden from
   assistive tech too.
   ========================================================================== */

.ile-tabs {
  display: flex;
  gap: var(--space-5);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-5);
  overflow-x: auto;
}
.ile-tab {
  position: relative;
  padding: 10px 0;
  border: 0;
  background: none;
  color: var(--ink-muted);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--t);
}
.ile-tab:hover { color: var(--ink); }
.ile-tab.is-active { color: var(--brand-ink); font-weight: 600; }
/* The underline matches the active nav link on the marketing site. */
.ile-tab.is-active::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--brand);
}
/* The hidden attribute has to beat every display rule in this file.
   The UA sheet's [hidden]{display:none} loses to any author class that sets
   display, so `.ile-btn` plus hidden left the button on screen - which is how
   a student with a recorded paper (no PDF) still saw "Open the PDF". Any
   element toggled with setAttribute('hidden') depends on this rule. */
[hidden] { display: none !important; }

.ile-tab-panel[hidden] { display: none; }


/* ==========================================================================
   21. TOAST
   Bottom right, stacked, one line each. Put the container in the page once and
   append toasts to it. Give it aria-live="polite".
   ========================================================================== */

.ile-toasts {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: min(380px, calc(100vw - 40px));
  pointer-events: none;
}
.ile-toast {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px var(--space-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink-muted);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-overlay);
  font-size: 13px;
  color: var(--ink-body);
  pointer-events: auto;
}
.ile-toast.is-success { border-left-color: var(--ok); }
.ile-toast.is-error { border-left-color: var(--bad); }
.ile-toast.is-info { border-left-color: var(--brand); }
.ile-toast-close { margin-left: auto; border: 0; background: none; color: var(--ink-muted); cursor: pointer; padding: 0 2px; }
.ile-toast-close:hover { color: var(--ink); }

/* Inline notice, for a message that belongs inside a card rather than
   floating over the page. */
.ile-notice {
  padding: 12px var(--space-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink-muted);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 13px;
  color: var(--ink-body);
}
.ile-notice.is-success { border-left-color: var(--ok); background: var(--ok-bg); color: var(--ok); }
.ile-notice.is-warn { border-left-color: var(--warn); background: var(--warn-bg); color: var(--warn); }
.ile-notice.is-error { border-left-color: var(--bad); background: var(--bad-bg); color: var(--bad); }


/* ==========================================================================
   22. EMPTY STATE
   ========================================================================== */

/* A new tutor's first screen is almost entirely empty states: on a fresh
   account eight of them cover 41% of the page. At the old size, each announced
   its own absence as loudly as a real section would, so the dashboard read as
   broken rather than simply new. They are quieter now, and sized to sit inside
   a card rather than fill it. */
/* It carries its own card surface. Most hosts in the portal are a .ile-card
   already and the empty state just sits inside one, but the dashboard panels
   mount into bare divs so that a populated panel can decide its own wrapper --
   and an empty state in a bare div was floating text in the middle of the
   page with nothing around it. Giving the surface to .ile-empty means every
   host gets the outlined box whether or not it happens to be a card. */
.ile-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: var(--space-6) var(--space-5);
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}
/* Inside a real card it is only the message again, so it drops the surface -
   cards never nest. */
.ile-card .ile-empty,
.ile-modal .ile-empty,
.ile-drawer .ile-empty {
  background: transparent;
  border: 0;
  border-radius: 0;
}
.ile-empty-icon { width: 22px; height: 22px; color: var(--ink-dim); }
.ile-empty-title { font-family: var(--font-ui); font-size: 14.5px; font-weight: 600; color: var(--ink-body); }
.ile-empty-text { font-size: 13.5px; line-height: 1.6; color: var(--ink-muted); max-width: 44ch; }
.ile-empty-action { margin-top: var(--space-3); }


/* ==========================================================================
   23. SKELETON
   Placeholder shapes while data loads. Give the container aria-busy="true".
   Under reduced motion the shimmer stops and it just sits there as a flat
   block, which is the correct behaviour.
   ========================================================================== */

.ile-skeleton {
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  animation: ile-pulse 1.4s ease-in-out infinite;
}
.ile-skeleton-line { height: 12px; margin-bottom: var(--space-2); }
.ile-skeleton-line.is-short { width: 40%; }
.ile-skeleton-line.is-half { width: 60%; }
.ile-skeleton-title { height: 22px; width: 240px; margin-bottom: var(--space-4); }
.ile-skeleton-block { height: 120px; border-radius: var(--radius); }
.ile-skeleton-row { height: 44px; border-radius: var(--radius); margin-bottom: var(--space-2); }
@keyframes ile-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }


/* ==========================================================================
   24. PAGINATION
   ========================================================================== */

.ile-pagination {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) 0;
}
.ile-pagination-info { font-size: 12px; color: var(--ink-muted); margin-right: auto; font-variant-numeric: tabular-nums; }
.ile-page-btn {
  min-width: 30px;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-body);
  font-family: var(--font-mono);
  font-size: 12px;
  cursor: pointer;
  transition: background var(--t), border-color var(--t), color var(--t);
}
.ile-page-btn:hover { background: var(--brand-tint); border-color: var(--line-strong); }
.ile-page-btn.is-active { background: var(--brand); border-color: var(--brand); color: var(--ink-invert); }
.ile-page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ile-page-gap { color: var(--ink-dim); padding: 0 2px; }


/* ==========================================================================
   25. CALENDAR
   Two views off one set of tokens. Month is a 7 column grid of day cells.
   Week is a time gutter plus 7 day columns with absolutely positioned events.

   POSITIONING NOTE for the week view: set the top and height of an event from
   JavaScript with el.style.top = '...' . That is a CSSOM write, not an inline
   style attribute in the HTML, so it is not affected by the style-src CSP and
   it does not stand in the way of dropping unsafe-inline later.
   ========================================================================== */

.ile-cal {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.ile-cal-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.ile-cal-title { font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--ink); }
.ile-cal-nav { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }

/* Column headings, Mon to Sun. Shared by both views. */
.ile-cal-dow-row {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.ile-cal-dow {
  padding: 8px var(--space-2);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  text-align: center;
}

/* ---- Month grid ---- */
.ile-cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.ile-cal-cell {
  min-height: 116px;
  padding: 6px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.ile-cal-cell:nth-child(7n) { border-right: 0; }
/* Days spilling in from the previous or next month. */
.ile-cal-cell.is-outside { background: var(--surface-2); }
/* --ink-dim fails AA and is for decorative marks only (see section 1) - this
   digit is real information (which month a padding day belongs to), so it
   gets --ink-muted, which passes AA, instead. */
.ile-cal-cell.is-outside .ile-cal-date { color: var(--ink-muted); }
.ile-cal-cell.is-weekend { background: var(--bg); }

.ile-cal-date {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  /* The cell is a flex column, so without this the date stretches to the full
     cell width and the round today marker comes out as an ellipse. */
  align-self: flex-start;
}
/* Today marker. A filled brand disc under the date, plus aria-current="date"
   on the cell so it is not colour alone. */
.ile-cal-cell.is-today .ile-cal-date {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--ink-invert);
  font-weight: 500;
}
.ile-cal-events { display: flex; flex-direction: column; gap: 3px; overflow: hidden; }
/* "+2 more" link at the bottom of a busy cell. */
.ile-cal-more { font-size: 11px; color: var(--brand-ink); cursor: pointer; background: none; border: 0; text-align: left; padding: 0; }

/* ---- All-day strip, above the time grid in the week view ---- */
.ile-cal-allday {
  display: grid;
  grid-template-columns: 56px repeat(7, minmax(0, 1fr));
  border-bottom: 1px solid var(--line);
  min-height: 32px;
}
.ile-cal-allday-label {
  padding: 6px 8px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  text-align: right;
  border-right: 1px solid var(--line);
}
.ile-cal-allday-cell { padding: 4px; border-right: 1px solid var(--line); display: flex; flex-direction: column; gap: 3px; }
.ile-cal-allday-cell:last-child { border-right: 0; }

/* ---- Week time grid ---- */
.ile-cal-week {
  display: grid;
  grid-template-columns: 56px repeat(7, minmax(0, 1fr));
  position: relative;
  max-height: 640px;
  overflow-y: auto;
}
.ile-cal-times { border-right: 1px solid var(--line); }
.ile-cal-hour {
  height: 48px;
  border-bottom: 1px solid var(--line);
  padding-right: 8px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-muted);
  transform: translateY(-6px);   /* sits the label on the line, not under it */
}
.ile-cal-col { position: relative; border-right: 1px solid var(--line); }
.ile-cal-col:last-child { border-right: 0; }
.ile-cal-col.is-today { background: var(--brand-tint); }
.ile-cal-slot { height: 48px; border-bottom: 1px solid var(--line); }

/* The now line. Set top from JS. */
.ile-cal-now { position: absolute; left: 0; right: 0; height: 1px; background: var(--bad); z-index: 3; pointer-events: none; }
.ile-cal-now::before {
  content: '';
  position: absolute;
  left: -3px;
  top: -2.5px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--bad);
}

/* ---- Event chip ----
   Works in a month cell (static) and in a week column (.is-positioned, with
   top and height set from JS). The status is carried by the left rule AND by
   the text inside, never by colour alone. */
.ile-event {
  display: block;
  width: 100%;
  padding: 3px 6px;
  border: 0;
  border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm);
  background: var(--brand-soft);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 12px;
  line-height: 1.35;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  overflow: hidden;
  transition: background var(--t);
}
.ile-event:hover { background: var(--surface-3); text-decoration: none; }
.ile-event.is-positioned { position: absolute; left: 2px; right: 2px; z-index: 2; padding: 4px 6px; }
.ile-event-time { font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.ile-event-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ile-event-meta { font-size: 11px; color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ile-event.is-scheduled { border-left-color: var(--brand); background: var(--brand-soft); }

/* Completed and no-show otherwise differ from scheduled by colour alone,
   which the note above this block says never happens. A checkmark and a
   dashed rule are the non-colour cues; is-cancelled already has its own (the
   strike-through below). */
.ile-event.is-completed { border-left-color: var(--ok); background: var(--ok-bg); }
.ile-event.is-completed .ile-event-title::before { content: '\2713\0020'; }
.ile-event.is-cancelled {
  border-left-color: var(--bad);
  background: var(--bad-bg);
}
.ile-event.is-cancelled .ile-event-title { text-decoration: line-through; }
.ile-event.is-no-show {
  border-left-color: var(--warn);
  border-left-style: dashed;
  background: var(--warn-bg);
}
.ile-event.is-no-show .ile-event-title::before { content: '!\0020'; }

/* Below 900px the month grid stops being a grid and the week view scrolls
   sideways inside its own container rather than pushing the page wide. The
   head is a title plus up to six controls (view group, today, prev, next,
   and New lesson when canCreate) with no flex-wrap, so below 900px they no
   longer fit one line: the title gets squeezed to a sliver, or - because
   .ile-cal has overflow: hidden for its rounded corners - the nav buttons are
   silently clipped off screen instead of wrapping. Letting the head wrap, and
   dropping the nav's margin-left: auto so it sits flush left on its own line
   rather than stranded on the right, fixes both. */
@media (max-width: 900px) {
  .ile-cal-cell { min-height: 88px; }
  .ile-cal-week,
  .ile-cal-allday { min-width: 720px; }
  .ile-cal-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .ile-cal-head { flex-wrap: wrap; }
  .ile-cal-nav { margin-left: 0; }
}


/* ==========================================================================
   26. AUTH SCREENS
   The login and password-reset pages. This is the seam a signed-in user
   actually notices: the old login page used marketing tokens and the
   dashboard behind it used portal tokens, so the two looked like different
   companies. Same buttons, same fields, same paper as everything else here.
   ========================================================================== */

/* Two panels: a cream one carrying the identity, a white one carrying the
   form. Built to mockup 6a.

   This page was a 452px card floating in an empty cream field with five
   stacked blocks of text under the button -- crowded and empty at once, which
   is what happens when a small box holds everything and the other nine
   hundred pixels hold nothing.

   The panel is cream rather than brand-filled on purpose. An earlier pass had
   it purple, which made the first thing a family sees the loudest surface in
   the product; cream against white is the same split done quietly, and it
   matches the page they have just come from.

   IMPORTANT: reset-password.html uses .ile-auth > .ile-auth-card with no
   aside, and carries an .ile-auth-back as a second child. .ile-auth is a
   centred column by default and only .is-split makes it the two-panel grid,
   so that page still lays out on its own. */
.ile-auth {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: var(--space-6);
  background: var(--bg);
}
.ile-auth.is-split {
  display: grid;
  grid-template-columns: 400px minmax(0, 1fr);
  align-items: stretch;
  gap: 0;
  padding: 0;
}

.ile-auth-aside {
  display: flex;
  flex-direction: column;
  padding: 38px 36px;
  background: var(--surface-2);
  border-right: 1px solid var(--line);
}
/* Pushed to the bottom of the panel, above the place line. The wordmark holds
   the top on its own. */
.ile-auth-pitch-wrap { margin-top: auto; }
.ile-auth-pitch {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 400;
  line-height: 1.18;
  letter-spacing: -0.01em;
  color: var(--ink);
  max-width: 15ch;
}
.ile-auth-pitch-sub {
  margin-top: var(--space-4);
  font-size: 14px;
  line-height: 1.65;
  color: var(--ink-body);
  max-width: 30ch;
}
.ile-auth-place {
  margin-top: auto;
  padding-top: var(--space-8);
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* The form column. White, so the split reads without a second border. */
.ile-auth.is-split .ile-auth-card {
  max-width: none;
  margin: 0;
  padding: 56px 60px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.ile-auth-card {
  width: 100%;
  max-width: 400px;
  margin: 0 auto;
  padding: 48px var(--space-6);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* Keeps the form from stretching across a very wide right-hand column. */
.ile-auth.is-split .ile-auth-card > * { width: 100%; max-width: 400px; }

.ile-auth-brand {
  display: block;
  font-family: var(--font-ui);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.26em;
  color: var(--ink);
  text-decoration: none;
}
.ile-auth-brand:hover { text-decoration: none; }
/* .ile-portal a sets brand-ink at (0,1,1) and beats a bare .ile-auth-brand at
   (0,1,0), so the wordmark came out purple. Same specificity trap as the h1
   weight. Qualified so it wins. */
.ile-portal .ile-auth-brand { color: var(--ink); }

/* The mark inside the form column, for when there is no panel beside it:
   portal.html below 900px, and reset-password.html at every width. */
.ile-auth-brand.is-compact { margin-bottom: var(--space-8); }
@media (min-width: 901px) {
  .ile-auth.is-split .ile-auth-brand.is-compact { display: none; }
}

.ile-auth-eyebrow {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 12px;
}
/* The status panes colour their eyebrow, because on those two screens the
   eyebrow IS the message and the heading only elaborates on it. */
.ile-auth-eyebrow.is-ok  { color: var(--ok); }
.ile-auth-eyebrow.is-bad { color: var(--bad); }

/* "What happened": the raw failure, kept out of the prose. The code goes in
   mono because it is the part someone reads out over the phone. */
.ile-auth-detail {
  background: var(--bad-bg);
  border: 1px solid rgba(158, 47, 34, 0.22);
  border-radius: 12px;
  padding: 14px 16px;
}
.ile-auth-detail-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bad);
  margin-bottom: 6px;
}
.ile-auth-detail-body {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-body);
  word-break: break-word;
}

/* The status shape: a narrow centred card, no panel. .ile-auth drops .is-split
   for these, so the default column layout above already centres it -- this
   sets the widths, the stacked buttons, and drops the panel.

   Hiding the aside is not optional. Without it the panel does not disappear,
   it stacks: the column layout puts four hundred pixels of "The portal for
   lessons, papers and invoices" directly above a message saying the link is
   dead. */
.ile-auth:not(.is-split) .ile-auth-aside { display: none; }
.ile-auth:not(.is-split) .ile-auth-card {
  max-width: 420px;
  padding: 52px 44px;
}
.ile-auth:not(.is-split) .ile-auth-title { font-size: 32px; line-height: 1.16; }
.ile-auth:not(.is-split) .ile-auth-form { gap: 11px; margin-top: 28px; }
.ile-auth:not(.is-split) .ile-auth-form .ile-btn.is-secondary {
  padding: 12px 22px;
  font-size: 13px;
}
.ile-auth-title {
  font-family: var(--font-display);
  font-size: 36px;
  font-weight: 400;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.ile-auth-sub {
  margin: 12px 0 0;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--ink-body);
}
.ile-auth-form { margin-top: 30px; display: grid; gap: 18px; }

/* Auth fields are the one place worth a bigger target: it is the first thing
   a family touches, and usually on a phone. */
.ile-auth-form .ile-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  margin-bottom: 8px;
}
.ile-auth-form .ile-input { padding: 13px 15px; font-size: 14px; border-radius: 10px; }
.ile-auth-form .ile-btn.is-primary { padding: 14px 22px; font-size: 14px; }

/* Label on the left, the reset link on the right, sharing one row. */
.ile-label-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
}
.ile-label-row .ile-label { margin-bottom: 8px; }
.ile-auth-forgot {
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brand-ink);
  cursor: pointer;
}
.ile-auth-forgot:hover { text-decoration: underline; }

.ile-auth-foot {
  margin-top: 26px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-muted);
}
/* A bookmark, not a door. dashboard-admin.html opens with requireRole(['admin'])
   and every admin query sits behind is_admin() in 003, so hiding this protects
   nothing. It just does not need to be the size of the sentence above it. */
.ile-auth-staff { margin-top: var(--space-2); font-size: 11.5px; }
.ile-auth-staff a { color: var(--ink-dim); }
.ile-auth-staff a:hover { color: var(--brand-ink); }

/* Only reset-password.html shows this, under its centred card. */
.ile-portal .ile-auth-back {
  font-size: 12.5px;
  letter-spacing: .04em;
  color: var(--ink-muted);
  text-decoration: none;
}
.ile-portal .ile-auth-back:hover { color: var(--brand-ink); }

/* Below 900 the panel goes rather than stacking: stacked, it would push the
   password field under the fold, and nobody scrolls past a wall of text to
   sign in. Mockup 10a, which is the phone sign-in, has no panel either. */
@media (max-width: 900px) {
  .ile-auth.is-split { display: block; padding: 0; background: var(--bg); }
  .ile-auth-aside { display: none; }
  .ile-auth.is-split .ile-auth-card {
    max-width: 460px;
    margin: 0 auto;
    padding: 44px var(--space-6) 40px;
    background: transparent;
    display: block;
  }
  .ile-auth.is-split .ile-auth-card > * { max-width: none; }
  .ile-auth-title { font-size: 33px; }
  .ile-auth-form .ile-input { padding: 15px 16px; font-size: 15px; border-radius: 12px; }
}


/* ==========================================================================
   27. UTILITIES
   ========================================================================== */

.ile-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
/* A question where marks were dropped. Amber, not red: losing four of nine is
   information, not an error, and a page of red marks reads as a telling-off. */
.ile-mono.is-dropped { color: var(--warn); }
/* Money and dates. Always one of these two, never plain body text. */
.ile-money { font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ile-date { font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A due date that has passed. Red text rather than a pill, because it sits in
   a date cell next to other dates and a pill there would out-shout the row. */
.ile-overdue { color: var(--bad); font-weight: 600; }
.ile-muted { color: var(--ink-muted); }
.ile-dim { color: var(--ink-dim); }
.ile-strong { color: var(--ink); font-weight: 600; }
.ile-small { font-size: 12px; }
.ile-nowrap { white-space: nowrap; }
.ile-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ile-right { text-align: right; }
.ile-divider { height: 1px; background: var(--line); border: 0; margin: var(--space-5) 0; }

/* Vertical and horizontal flow helpers, so pages stop inventing margins. */
.ile-stack { display: flex; flex-direction: column; gap: var(--gap); }
.ile-stack.is-tight { gap: var(--space-2); }
.ile-inline { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.ile-inline.is-between { justify-content: space-between; }
.ile-push { margin-left: auto; }

/* Visible to screen readers only. Use for the label on an icon-only control
   and for live-region text. */
.ile-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;
}


/* ==========================================================================
   28. DASHBOARD LAYER
   The four signed-in overview screens. Everything above this point is the
   general kit -- tables, forms, modals -- and is used across every page in the
   portal. What follows is only for the dashboards, and exists because those
   four screens have a job the kit does not: they have to answer a question in
   the first screenful, and the kit is deliberately even-handed about what it
   shows.

   Three ideas hold it together:

     One filled brand surface per screen. The next lesson is the answer on
     three of the four dashboards, so it gets the fill and nothing else does.
     The admin has no single next thing -- six lessons, not one -- so it has
     no block, and its timetable is the answer instead.

     Statuses are words. A word can be read; a coloured dot has to be decoded
     against a legend nobody has.

     Numbers come with a sentence. "34" tells you nothing. "34 -- four more
     than last week" is the whole point of showing it.
   ========================================================================== */

/* ---- 28.1 The dashboard grid ----
   Main column and a rail. The rail is fixed at 360px rather than a fraction
   so the reading measure of the main column does not stretch on a wide
   monitor. Below 1100 it collapses and the rail flows underneath in source
   order, which is why the rail's contents are written least-urgent-last. */
.ile-dash {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: 34px;
  align-items: start;
}
.ile-dash-main { display: flex; flex-direction: column; gap: 32px; min-width: 0; }
.ile-dash-rail { display: flex; flex-direction: column; gap: 20px; min-width: 0; }

@media (max-width: 1100px) {
  .ile-dash { grid-template-columns: minmax(0, 1fr); gap: 26px; }
}
@media (max-width: 640px) {
  .ile-dash-main { gap: 26px; }
}


/* ---- 28.2 The next-lesson block ----
   The one filled brand surface. Time on the left, what it is in the middle,
   the action on the right, split by a hairline in the cream. */
.ile-nextup {
  display: flex;
  align-items: center;
  gap: 26px;
  padding: 26px 30px;
  border-radius: 16px;
  background: var(--brand);
  color: var(--ink-invert);
}
.ile-nextup-when { flex-shrink: 0; text-align: left; }
.ile-nextup-time {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-invert);
}
/* "In two hours" rather than only a clock time: the relative distance is what
   tells someone whether to act now, and the clock alone makes them do the
   subtraction. */
.ile-nextup-rel {
  margin-top: 7px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #C9B8ED;
}
.ile-nextup-rule {
  flex-shrink: 0;
  align-self: stretch;
  width: 1px;
  background: rgba(251, 248, 243, 0.28);
}
.ile-nextup-body { flex: 1; min-width: 0; }
.ile-nextup-title {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink-invert);
}
.ile-nextup-detail {
  margin-top: 5px;
  font-size: 13.5px;
  line-height: 1.55;
  color: #D8CFF2;
}
.ile-nextup-actions {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}

@media (max-width: 640px) {
  .ile-nextup { gap: 13px; padding: 16px; border-radius: var(--radius-card); align-items: center; }
  .ile-nextup-when { flex: 0 0 auto; }
  .ile-nextup-time { font-size: 27px; }
  .ile-nextup-rel { margin-top: 4px; font-size: 9px; letter-spacing: 0.08em; }
  .ile-nextup-title { font-size: 16px; line-height: 1.25; }
  .ile-nextup-detail { margin-top: 4px; font-size: 12px; line-height: 1.45; }
  /* One action stays on the row but stops setting the block's height: without
     the cap a long subject line pushes the pill into a column of its own and
     the whole block grows to 200px tall. */
  .ile-nextup-actions .ile-btn { padding: 9px 16px; min-height: 40px; }
  /* Two actions will not fit beside the body on a phone, so they drop to a
     full-width row underneath and stay 44px tall. */
  .ile-nextup.has-two-actions { flex-wrap: wrap; }
  .ile-nextup.has-two-actions .ile-nextup-actions {
    flex-direction: row;
    width: 100%;
    margin-top: 4px;
  }
  .ile-nextup.has-two-actions .ile-nextup-actions .ile-btn { flex: 1; min-height: 44px; }
}


/* ---- 28.3 Status words ----
   Replaces .ile-pill everywhere inside a table or a list row. The pill is kept
   for calendars and drawers, where a row of bare words would have nothing to
   sit against. */
.ile-status {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
}
.ile-status.is-complete,
.ile-status.is-completed,
.ile-status.is-done,
.ile-status.is-paid       { color: var(--ok); }
.ile-status.is-overdue,
.ile-status.is-cancelled,
.ile-status.is-unassigned { color: var(--bad); }
.ile-status.is-needs-review,
.ile-status.is-due-today  { color: var(--warn); }

/* The two exceptions that stay pills, because each is the one thing on its row
   the reader is meant to notice. */
.ile-tag-next,
.ile-tag-due {
  display: inline-block;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.ile-tag-next { background: var(--surface); color: var(--brand-ink); }
.ile-tag-due  { background: var(--warn-bg); color: var(--warn); }


/* ---- 28.4 Grouped list rows ----
   A card holding rows separated by hairlines. Used for the numbered lists
   (admin's outstanding matters, a tutor's owed notes) and the student's
   checklist. The card is .ile-card.is-flush so rows can reach its edges. */
.ile-rows { display: block; }
.ile-row {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr) auto;
  align-items: center;
  gap: 4px 14px;
  padding: 17px 22px;
  border-bottom: 1px solid var(--line);
}
.ile-rows > .ile-row:last-child { border-bottom: 0; }
/* Rows without a leading index or checkbox. */
.ile-row.is-plain { grid-template-columns: minmax(0, 1fr) auto; }

.ile-row-index {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  align-self: start;
  padding-top: 2px;
}
.ile-row-body { min-width: 0; }
.ile-row-title {
  font-size: 14.5px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink);
}
.ile-row-detail {
  margin-top: 4px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-muted);
}
.ile-row-end {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Done work stays in the list at full strength, with the title struck and the
   colour dropped. Fading it to 40% opacity makes it unreadable, which is a
   strange reward for having finished it. */
.ile-row.is-done .ile-row-title {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  color: var(--ink-muted);
  font-weight: 400;
}
.ile-row.is-done .ile-row-detail { color: var(--ink-dim); }

/* The checkbox on the student's work list. A real input, kept operable, drawn
   over rather than replaced, so it keeps its keyboard behaviour.

   Named .ile-tick, NOT .ile-check: .ile-check is already the wrapper <label>
   in the FORMS section that pairs a control with its text. Reusing the name
   put appearance:none and an 18px square on that label, which collapsed every
   "include archived and inactive" row in the portal into a vertical stack of
   single letters. */
.ile-tick {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  display: grid;
  place-content: center;
  transition: background var(--t), border-color var(--t);
}
.ile-tick:hover { border-color: var(--brand); }
.ile-tick:checked { background: var(--ok); border-color: var(--ok); }
.ile-tick:checked::after {
  content: '';
  width: 9px;
  height: 5px;
  border-left: 2px solid #FFFFFF;
  border-bottom: 2px solid #FFFFFF;
  transform: rotate(-45deg) translate(1px, -1px);
}
.ile-tick:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

@media (max-width: 640px) {
  .ile-row { padding: 14px 16px; column-gap: 11px; }
  /* The action moves under the detail line so it can be a full-height pill a
     thumb can actually hit. */
  .ile-row.has-action {
    grid-template-columns: 30px minmax(0, 1fr);
  }
  .ile-row.has-action .ile-row-end {
    grid-column: 2;
    justify-self: start;
    margin-top: 10px;
  }
  .ile-row.has-action .ile-row-end .ile-btn { min-height: 40px; }
}


/* ---- 28.5 Ruled key/value lists ----
   The rail's workhorse: family to amount, tutor to hours, day to time. A list,
   not a table, because there are two columns and no header worth writing. */
.ile-keyrows { display: block; }
.ile-keyrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
}
.ile-keyrow:last-child { border-bottom: 0; }
.ile-keyrow-key { color: var(--ink-body); min-width: 0; }
.ile-keyrow-key small {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--ink-muted);
}
.ile-keyrow-val {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ile-keyrow-val.is-muted { color: var(--ink-muted); }


/* ---- 28.6 The tutor's note ----
   The most-read element in the product, so it gets the largest body type in
   it. Prose, not a quotation: no quote marks, no italics, no accent border.
   Someone wrote this about a child and it should look like writing. */
.ile-note-body {
  font-family: var(--font-display);
  font-size: 18.5px;
  font-weight: 400;
  line-height: 1.65;
  color: var(--ink);
}
.ile-note-by {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--ink-muted);
}
@media (max-width: 640px) {
  /* Down to 16px, not to body size. Shrinking this to 14px is what would make
     it look like a system message rather than something a person wrote. */
  .ile-note-body { font-size: 16px; }
}


/* ---- 28.7 Standing bars ----
   A percentage against the mark that counts as on-track. The tick is the whole
   point: a bar with no target is a bar you cannot be doing well or badly
   against. */
.ile-standing { display: block; }
.ile-standing-row { padding: 12px 0; border-bottom: 1px solid var(--line); }
.ile-standing-row:last-child { border-bottom: 0; }
.ile-standing-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: 9px;
}
.ile-standing-label { font-size: 13.5px; color: var(--ink-body); }
.ile-standing-pct {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ile-track {
  position: relative;
  height: 5px;
  border-radius: var(--radius-pill);
  background: #F1EEF6;
  overflow: visible;
}
.ile-track-fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--brand);
}
.ile-track-tick {
  position: absolute;
  top: -3px;
  width: 1px;
  height: 11px;
  background: var(--ink);
}


/* ---- 28.8 Sparkline cards ----
   Every recorded mark, oldest to newest. One stroke, no axis, no dots, no
   fill: at this size the only readable question is "which way is it going",
   and anything else drawn on it answers a question nobody asked. */
.ile-sparks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.ile-spark {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 18px 20px;
}
.ile-spark-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ile-spark-figs { display: flex; align-items: baseline; gap: 8px; margin-top: 8px; }
.ile-spark-pct {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 400;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.ile-spark-delta { font-family: var(--font-mono); font-size: 12px; }
.ile-spark-delta.is-up { color: var(--ok); }
.ile-spark-delta.is-down { color: var(--bad); }
.ile-spark-graph { margin-top: 12px; height: 32px; }
.ile-spark-graph svg { display: block; width: 100%; height: 32px; overflow: visible; }

@media (max-width: 640px) {
  /* Three across at phone width leaves each graph 90px wide, which is not a
     line, it is a smudge. One per row, text left and the stroke right. */
  .ile-sparks { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .ile-spark {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 104px;
    align-items: center;
    gap: 2px var(--space-4);
    padding: 14px 16px;
  }
  /* Placed explicitly. Left to auto-placement the three children fill the two
     columns in source order and the graph drops onto its own row underneath,
     full width, which is the one thing this layout exists to avoid. */
  .ile-spark-label { grid-column: 1; grid-row: 1; }
  .ile-spark-figs  { grid-column: 1; grid-row: 2; margin-top: 2px; }
  .ile-spark-graph { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; }
}


/* ---- 28.8b Column chart ----
   Every recorded mark, oldest to newest. Columns rather than the sparkline
   used on the dashboard, because this screen is where someone comes to read
   the individual numbers rather than the shape -- so each column carries its
   own value and only the most recent one is filled in brand.

   No y-axis. The columns are a percentage of a hundred and each one is
   labelled, so an axis would be a third way of saying the same thing. */
.ile-colchart-wrap {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 26px 26px 18px;
}
.ile-colchart {
  display: flex;
  align-items: flex-end;
  gap: 14px;
  height: 190px;
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}
.ile-colchart-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
  gap: 7px;
  min-width: 0;
}
.ile-colchart-val {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-muted);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.ile-colchart-bar {
  background: var(--surface-2);
  border-radius: 6px 6px 0 0;
  min-height: 2px;
}
.ile-colchart-col.is-latest .ile-colchart-bar { background: var(--brand); }
.ile-colchart-col.is-latest .ile-colchart-val { color: var(--brand-ink); }

.ile-colchart-labels { display: flex; gap: 14px; margin-top: 10px; }
.ile-colchart-label {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 640px) {
  .ile-colchart-wrap { padding: 18px 16px 14px; }
  .ile-colchart { height: 140px; gap: 7px; }
  .ile-colchart-labels { gap: 7px; }
  /* Below about eight columns the labels stop being readable at all, so they
     go rather than overlapping into mush. The values above the bars stay. */
  .ile-colchart-label { font-size: 8.5px; letter-spacing: 0; }
  .ile-colchart-val { font-size: 9.5px; }
}


/* ---- 28.9 The upload prompt ----
   Dashed, so it reads as somewhere to put something rather than as another
   card of information. The full dropzone stays on the Papers page. */
.ile-upload {
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-card);
  padding: 22px 20px;
  text-align: center;
  background: var(--surface);
}
.ile-upload-icon { color: var(--brand); margin-bottom: 10px; }
.ile-upload-title {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 400;
  color: var(--ink);
}
.ile-upload-text {
  margin: 6px auto 14px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-muted);
  max-width: 34ch;
}


/* ---- 28.10 Mark figures ----
   "72 / 100" rather than "72%": the denominator is what makes a mark
   comparable, and half the papers here are not out of a hundred. */
.ile-mark { display: flex; align-items: baseline; gap: 6px; }
.ile-mark-value {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 400;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.ile-mark-of { font-family: var(--font-mono); font-size: 13px; color: var(--ink-muted); }
.ile-delta.is-up { color: var(--ok); }
.ile-delta.is-down { color: var(--bad); }


/* ---- 28.11 The mobile action bar ----
   One per screen, and only on a phone. On a desktop the same action lives in
   the page head or the rail; down here the top of the screen is out of thumb
   reach, so the single thing the user came to do follows them down the page.
   The 22px bottom padding is clearing the home indicator, not decoration. */
.ile-mobilebar { display: none; }

@media (max-width: 640px) {
  .ile-mobilebar {
    display: flex;
    position: sticky;
    bottom: 0;
    z-index: 20;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-6) calc(var(--gutter) * -1) calc(30px * -1);
    padding: 14px var(--gutter) 22px;
    background: var(--surface);
    border-top: 1px solid var(--line);
  }
  .ile-mobilebar .ile-btn { flex: 1; min-height: 44px; }
  /* The parent's bar carries the amount as well as the action, so the button
     does not stretch. */
  .ile-mobilebar.is-amber {
    background: var(--warn-bg);
    border-top-color: rgba(138, 90, 18, 0.24);
  }
  .ile-mobilebar.is-amber .ile-btn { flex: 0 0 auto; min-width: 96px; }
  .ile-mobilebar-amount { flex: 1; min-width: 0; }
  .ile-mobilebar-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--warn);
  }
  .ile-mobilebar-value {
    margin-top: 3px;
    font-family: var(--font-mono);
    font-size: 17px;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
}


/* ---- 28.12 Tables on a phone ----
   Stacked rows inside the same card, never a sideways scroll. A table you have
   to drag horizontally on a phone is a table nobody reads the right-hand end
   of, which is where the status usually is.

   The first cell (time or reference) holds the left column, the last cell
   (status) the right, and everything between stacks in the middle: the first
   of them is the subject, the rest are the people. */
@media (max-width: 640px) {
  .ile-table.is-ref-first { min-width: 0; }
  .ile-table.is-ref-first { display: block; }
  .ile-table.is-ref-first thead { display: none; }
  .ile-table.is-ref-first tbody { display: block; }

  .ile-table.is-ref-first tr {
    display: grid;
    grid-template-columns: 52px minmax(0, 1fr) auto;
    align-items: start;
    column-gap: 12px;
    padding: 13px 0;
    border-bottom: 1px solid var(--line);
  }
  .ile-table.is-ref-first tbody tr:last-child { border-bottom: 0; }
  .ile-table.is-ref-first td { display: block; padding: 0; border: 0; }

  .ile-table.is-ref-first td:first-child {
    grid-column: 1;
    grid-row: 1 / span 9;
    width: auto;
    padding-top: 1px;
  }
  .ile-table.is-ref-first td:last-child {
    grid-column: 3;
    grid-row: 1 / span 9;
    align-self: center;
    text-align: right;
  }
  .ile-table.is-ref-first td:not(:first-child):not(:last-child) {
    grid-column: 2;
    font-size: 12.5px;
    color: var(--ink-muted);
  }
  /* The first of the middle cells is what the row is about. */
  .ile-table.is-ref-first td:nth-child(2):not(:last-child) {
    font-size: 14px;
    color: var(--ink);
    margin-bottom: 2px;
  }

  .ile-table.is-ref-first tr.is-current { padding-left: 10px; padding-right: 10px; border-radius: var(--radius-row); background: var(--brand-soft); }
  .ile-table.is-ref-first tr.is-current td { background: transparent; }
  .ile-table.is-ref-first tr.is-current td:first-child,
  .ile-table.is-ref-first tr.is-current td:last-child { border-radius: 0; }
}


/* ---- 28.13 The phone top bar ----
   The name goes; sign out stays.

   An earlier pass hid the sign-out chip on a phone and moved it inside the
   slide-in menu, on the reasoning that it was eating a 375px row. It was
   eating that row alongside the user's full name, and the name is the half
   nobody needs -- they know who they are. Hiding the control instead put a
   thing people deliberately go looking for two taps deep behind a hamburger,
   where the first person to look for it could not find it. Space was never
   the problem worth solving at the cost of that.

   These live down here rather than beside the rest of the shell because
   .ile-identity is defined in section 14, and an override written at section 4
   loses to it on source order alone. */
@media (max-width: 640px) {
  .ile-topnav { gap: var(--space-2); }
  .ile-identity { display: none; }
}
/* The role chip stays. Measured at 320px, the narrowest phone worth
   supporting: wordmark, chip, sign-out and the menu button come to about
   250px inside a 280px content box, so there is nothing to reclaim by
   dropping it. If a longer role word ever changes that, the chip is what
   goes -- it labels which portal you are in, which the content already says,
   whereas the way out does not exist anywhere else on the screen. */


/* ==========================================================================
   29. PRINT
   For invoices and progress reports. Everything that is chrome comes off, the
   paper goes white, and the ink goes black so it does not eat toner.
   Put .ile-print-doc on the element that should survive.
   ========================================================================== */

@media print {
  :root {
    --bg: #FFFFFF;
    --surface: #FFFFFF;
    --surface-2: #FFFFFF;
    --surface-3: #FFFFFF;
    --ink: #000000;
    --ink-body: #1A1A1A;
    --ink-muted: #444444;
    --line: #C9C9C9;
    --line-strong: #9A9A9A;
    --shadow-btn: none;
    --shadow-overlay: none;
  }

  .ile-sidebar,
  .ile-topbar,
  .ile-scrim,
  .ile-toasts,
  .ile-drawer,
  .ile-modal-backdrop,
  .ile-tabs,
  .ile-pagination,
  .ile-page-actions,
  .ile-card-actions,
  .ile-section-actions,
  .ile-btn,
  .ile-btn-icon,
  .ile-menu-btn,
  .ile-no-print { display: none !important; }

  .ile-app { display: block; }
  .ile-content { max-width: none; padding: 0; }
  .ile-print-only { display: block !important; }

  .ile-card { border: 1px solid var(--line); break-inside: avoid; }
  .ile-table { min-width: 0; font-size: 11pt; }
  .ile-table-wrap { overflow: visible; }
  .ile-table thead { display: table-header-group; }   /* repeat header on each page */
  .ile-table tr { break-inside: avoid; }
  .ile-pill { border: 1px solid var(--line-strong); background: none !important; color: #000 !important; }
  .ile-pill::before { display: none; }

  .ile-print-doc { padding: 0; }
  .ile-print-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    padding-bottom: 16pt;
    margin-bottom: 16pt;
    border-bottom: 1pt solid var(--line-strong);
  }
  .ile-print-foot {
    margin-top: 20pt;
    padding-top: 10pt;
    border-top: 1pt solid var(--line);
    font-size: 9pt;
    color: var(--ink-muted);
  }
  a[href]::after { content: ''; }   /* no printed URLs after every link */

  @page { margin: 16mm; }
}

.ile-print-only { display: none; }


/* ==========================================================================
   30. LEGACY ALIASES
   The three existing dashboards are full of --ile-* variables and the old
   class names. These aliases keep those pages readable while they are being
   rewritten one at a time. DELETE THIS WHOLE BLOCK once every dashboard has
   been moved onto the class names above. Nothing new should use it.
   ========================================================================== */

:root {
  --ile-ink:          var(--ink);
  --ile-purple:       var(--brand);
  --ile-purple-mid:   var(--brand-hover);
  --ile-purple-deep:  var(--brand-ink);
  --ile-lavender:     var(--bg);
  --ile-lavender2:    var(--surface-2);
  --ile-white:        var(--surface);
  --ile-success:      var(--ok);
  --ile-success-bg:   var(--ok-bg);
  --ile-amber:        var(--warn);
  --ile-amber-bg:     var(--warn-bg);
  --ile-red:          var(--bad);
  --ile-red-bg:       var(--bad-bg);
  --ile-border:       var(--line);
  --ile-text-muted:   var(--ink-muted);
  --ile-font-display: var(--font-display);
  --ile-font-body:    var(--font-ui);
  --ile-font-mono:    var(--font-mono);
  --ile-grad-purple:  var(--brand);   /* flat now, the gradient is gone on purpose */
  --ile-shadow-card:  none;
  --ile-shadow-card-hover: none;
  --ile-shadow-btn:   var(--shadow-btn);
}

/* Old class names mapped onto the new look. */
.status-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--radius-pill); font-family: var(--font-ui); font-size: 12px; font-weight: 600; background: var(--neutral-bg); color: var(--neutral); }
.status-pill::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.status-pill.paid, .status-pill.active, .status-pill.confirmed { background: var(--ok-bg); color: var(--ok); }
.status-pill.pending { background: var(--warn-bg); color: var(--warn); }
.status-pill.overdue, .status-pill.cancelled { background: var(--bad-bg); color: var(--bad); }
.status-pill.paused { background: var(--neutral-bg); color: var(--neutral); }

.ile-card:hover { box-shadow: none; transform: none; }   /* kills the old hover lift */
.ile-btn.primary { background: var(--brand); color: var(--ink-invert); box-shadow: var(--shadow-btn); }
.ile-btn.primary:hover { background: var(--brand-hover); transform: none; }
.ile-btn.outline { background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink-body); }
.ile-btn.outline:hover { background: var(--brand-tint); border-color: var(--brand); transform: none; box-shadow: none; }
.ile-btn.danger { background: none; border: 1px solid rgba(158, 47, 34, 0.35); color: var(--bad); }
.ile-btn.danger:hover { background: var(--bad-bg); transform: none; }
.ile-btn.block { width: 100%; }
.ile-field label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-muted); font-weight: 600; margin-bottom: 6px; }
.ile-field input, .ile-field select, .ile-field textarea { padding: 10px 12px; background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink-body); font-family: var(--font-ui); font-size: 14px; border-radius: var(--radius); width: 100%; }
.ile-field input:focus, .ile-field select:focus, .ile-field textarea:focus { border-color: var(--brand); }
.ile-field-hint { font-size: 12px; color: var(--ink-muted); margin-top: 5px; }

/* The old dashboards keep a single .ile-toast in the page and reveal it with
   .show, so a bare .ile-toast has to start hidden. New code appends toasts
   into a .ile-toasts container, which is what restores the flex layout. */
.ile-toast { display: none; }
.ile-toast.show,
.ile-toasts .ile-toast { display: flex; }
