/*
 * Irtiqa component layer
 * ---------------------------------------------------------------------------
 * Every rule resolves to a token from tokens.css. Selectors are kept to a
 * single class where possible so specificity stays flat and later phases can
 * add components without fighting cascade order.
 */

/* --- Reset -------------------------------------------------------------- */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-family: var(--font-sans);
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  color: var(--text);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, svg, video { display: block; max-width: 100%; }

button, input, select, textarea { font: inherit; color: inherit; }

a { color: var(--teal); text-decoration: none; }
a:hover { color: var(--ochre); }

/* One focus treatment everywhere. Visible-only so pointer users are not
   shown rings they did not ask for, but keyboard navigation always is. */
:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
:focus:not(:focus-visible) { outline: none; }

/* --- Typography --------------------------------------------------------- */

.display {
  font-family: var(--font-display);
  font-size: var(--t-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  font-weight: var(--w-regular);
}

.title {
  font-family: var(--font-display);
  font-size: var(--t-2xl);
  line-height: var(--lh-tight);
  font-weight: var(--w-regular);
}

.heading {
  font-size: var(--t-md);
  font-weight: var(--w-semibold);
  line-height: var(--lh-snug);
}

/* The eyebrow label — 10px, uppercase, widely tracked. Used for section
   headers, table column groups and metric captions. */
.label {
  font-size: var(--t-micro);
  font-weight: var(--w-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-subtle);
}

.muted   { color: var(--text-muted); }
/* A single number under a small caption — queue depths, counts. Tabular
   figures so a column of them lines up as it changes. */
.stat {
  font-size: var(--t-xl, 1.5rem);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.text-success { color: var(--success); }
.text-danger  { color: var(--danger); }
.subtle  { color: var(--text-subtle); }
.numeral { font-family: var(--font-serif); font-variant-numeric: tabular-nums; }
.mono    { font-family: var(--font-mono); font-size: var(--t-sm); }

/* --- Buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 9px var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--t-base);
  font-weight: var(--w-medium);
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}

.btn:disabled,
.btn[aria-busy="true"] {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--ochre);
  color: var(--ink-deep);
  font-weight: var(--w-semibold);
}
.btn-primary:hover:not(:disabled) { background: var(--ochre-hover); color: #fff; }

.btn-secondary {
  background: var(--card);
  border-color: var(--line-strong);
  color: var(--text);
}
.btn-secondary:hover:not(:disabled) { background: var(--paper-sunken); border-color: var(--teal); }

.btn-ghost { background: transparent; color: var(--teal); }
.btn-ghost:hover:not(:disabled) { background: var(--sage); }

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: #8f3818; }

.btn-sm { padding: 6px var(--s-3); font-size: var(--t-sm); border-radius: var(--r-sm); }
.btn-lg { padding: 12px var(--s-6); font-size: var(--t-md); }
.btn-block { width: 100%; }

/* Spinner shown in place of the label while a request is in flight. The
   button keeps its width so the layout does not jump. */
.btn__spinner {
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: irtiqa-spin 620ms linear infinite;
}

@keyframes irtiqa-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .btn__spinner { animation-duration: 1600ms; }
}

/* --- Cards -------------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
}

.card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--line);
}

.card__body   { padding: var(--s-5); }
.card__footer { padding: var(--s-4) var(--s-5); border-top: 1px solid var(--line); background: var(--paper-raised); }

/* --- Forms -------------------------------------------------------------- */

.field { margin-bottom: var(--s-4); }

.field__label {
  display: block;
  margin-bottom: 6px;
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  color: var(--text);
}

.field__label .required { color: var(--danger); margin-inline-start: 2px; }

.input,
.select,
.textarea {
  width: 100%;
  padding: 9px var(--s-3);
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  font-size: var(--t-base);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}

.input:hover, .select:hover, .textarea:hover { border-color: rgba(16, 28, 27, 0.24); }

.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--teal);
  box-shadow: 0 0 0 3px rgba(23, 96, 95, 0.12);
}

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

.select {
  appearance: none;
  padding-inline-end: var(--s-8);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23101c1b' fill-opacity='.5' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-3) center;
}

/* Invalid state is applied by the JS layer from the server's errors object,
   so what the user sees always reflects the server's decision. */
.input.is-invalid,
.select.is-invalid,
.textarea.is-invalid {
  border-color: var(--danger);
  background: var(--danger-tint);
}

.field__error {
  display: none;
  margin-top: 5px;
  font-size: var(--t-sm);
  color: var(--danger);
}
.field__error.is-visible { display: block; }

.field__hint { margin-top: 5px; font-size: var(--t-sm); color: var(--text-subtle); }

/* --- Badges ------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  font-size: var(--t-xs);
  font-weight: var(--w-semibold);
  letter-spacing: 0.02em;
}

.badge-neutral { background: var(--paper-sunken); color: var(--text-muted); }
.badge-success { background: var(--success-tint); color: var(--success); }
.badge-warning { background: var(--warning-tint); color: var(--warning); }
.badge-danger  { background: var(--danger-tint);  color: var(--danger); }
.badge-info    { background: var(--info-tint);    color: var(--info); }

/* Pulsing dot for anything happening right now — a live exam, an upload. */
.badge-live { background: var(--ochre-tint); color: var(--ochre-deep); }
.badge-live::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ochre);
  animation: irtiqa-pulse 1.8s var(--ease) infinite;
}

@keyframes irtiqa-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* --- Tables ------------------------------------------------------------- */

/* overflow-x rather than overflow:hidden. The radius still needs the box to
 * clip, but `hidden` clips in the one direction where clipping loses data:
 * a table wider than its wrapper had its last columns cut off with no way to
 * scroll to them. At 768px the users table measured 805px inside a 734px
 * wrapper, so the actions column — edit, reset password — was simply gone.
 *
 * `auto` makes that band reachable instead of silently truncated. It is a net,
 * not the plan: the stacked layout below is what tablets and phones are meant
 * to get, and this catches whatever is still too wide at any width, including
 * a future table with more columns than the ones here now.
 *
 * Setting overflow-x to auto computes overflow-y to auto as well, which would
 * clip anything drawn outside the box. Nothing inside a row is positioned out
 * of flow — row actions open modals, which the modal layer appends to body —
 * so there is nothing here to clip.
 */
.table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  overflow-x: auto;
}

.table { width: 100%; border-collapse: collapse; }

.table thead th {
  padding: var(--s-3) var(--s-4);
  background: var(--paper-sunken);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-micro);
  font-weight: var(--w-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-subtle);
  text-align: start;
  white-space: nowrap;
}

.table tbody td {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--paper-raised); }

.table .num { text-align: end; font-variant-numeric: tabular-nums; }

/* The column that holds row controls rather than a value. Named so the mobile
   rules below can tell "this cell has a button in it" from "this cell has a
   number in it" — the two want opposite treatment once the table stacks. */
.table .table__actions { text-align: end; white-space: nowrap; }

/* Below this the table re-flows into stacked rows, each cell labelled from its
   data-label. Avoids the horizontal scroll the brief rules out while keeping
   one markup path.
 
   900px, not the 720px this used to be. The number is not a device size — it
   is where these tables stop fitting. Measured on the users table, which is
   the widest: 805px of columns needs a viewport of about 880px once the
   gutters are taken off, so every width below that was showing a table too
   wide for its wrapper. Stacking stopped at 720px, which left 721–880px — the
   whole tablet band, iPad portrait included — rendering a desktop table that
   did not fit. Raising this to 900px closes the gap rather than papering over
   it with a scrollbar. */
@media (max-width: 900px) {
  .table thead { display: none; }
  .table, .table tbody, .table tr, .table td { display: block; width: 100%; }
  .table tbody tr {
    padding: var(--s-2) 0;
    border-bottom: 1px solid var(--line);
  }
  .table tbody td {
    display: flex;
    justify-content: space-between;
    gap: var(--s-4);
    border: none;
    padding: 6px var(--s-4);
  }
  .table tbody td::before {
    content: attr(data-label);
    font-size: var(--t-micro);
    font-weight: var(--w-semibold);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--text-subtle);
  }

  /* A cell with no label to give would otherwise draw an empty ::before and
     let space-between push its content against the right edge, away from the
     column of values it belongs to. Nothing is a legitimate label — an action
     cell reading "ACTIONS  Remove" is worse than the button alone — so those
     cells opt out of the label rather than being given a filler one. */
  .table tbody td:not([data-label])::before,
  .table tbody td[data-label=""]::before,
  .table tbody td.table__actions::before { content: none; }

  .table tbody td:not([data-label]),
  .table tbody td[data-label=""],
  .table tbody td.table__actions {
    justify-content: flex-start;
    text-align: start;
  }

  /* Controls get room to breathe once they are on their own line. */
  .table tbody td.table__actions {
    flex-wrap: wrap;
    gap: var(--s-2);
    padding-top: var(--s-2);
    white-space: normal;
  }

  .table .num { text-align: start; }
}

/* --- Toasts ------------------------------------------------------------- *
 * Anchored directly below the header, per the brief. Fixed positioning with a
 * header-height offset so the stack sits in the same place on every screen.  */

.toast-stack {
  position: fixed;
  top: calc(var(--header-h) + var(--s-3));
  inset-inline-end: var(--s-5);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  width: min(380px, calc(100vw - var(--s-8)));
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-inline-start: 3px solid var(--text-subtle);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lifted);
  pointer-events: auto;
  animation: toast-in var(--t-slow) var(--ease-out);
}

.toast.is-leaving { animation: toast-out var(--t-base-ms) var(--ease) forwards; }

.toast--success { border-inline-start-color: var(--success); }
.toast--error   { border-inline-start-color: var(--danger); }
.toast--warning { border-inline-start-color: var(--warning); }
.toast--info    { border-inline-start-color: var(--teal); }

.toast__icon { flex-shrink: 0; width: 18px; height: 18px; margin-top: 1px; }
.toast--success .toast__icon { color: var(--success); }
.toast--error   .toast__icon { color: var(--danger); }
.toast--warning .toast__icon { color: var(--warning); }
.toast--info    .toast__icon { color: var(--teal); }

.toast__content { flex: 1; min-width: 0; }
.toast__title   { font-size: var(--t-base); font-weight: var(--w-semibold); }
.toast__message { margin-top: 2px; font-size: var(--t-sm); color: var(--text-muted); word-wrap: break-word; }

.toast__close {
  flex-shrink: 0;
  padding: 2px;
  background: none;
  border: none;
  color: var(--text-subtle);
  cursor: pointer;
  line-height: 1;
}
.toast__close:hover { color: var(--text); }

@keyframes toast-in {
  from { opacity: 0; transform: translateX(calc(16px * var(--flip))) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

@keyframes toast-out {
  to { opacity: 0; transform: translateX(calc(16px * var(--flip))); }
}

@media (max-width: 560px) {
  .toast-stack {
    inset-inline-end: var(--s-3);
    inset-inline-start: var(--s-3);
    width: auto;
  }
}

/* --- Modals ------------------------------------------------------------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  background: rgba(13, 47, 48, 0.55);
  backdrop-filter: blur(2px);
  animation: fade-in var(--t-base-ms) var(--ease);
}

.modal-backdrop.is-leaving { animation: fade-out var(--t-fast) var(--ease) forwards; }

.modal {
  position: relative;
  z-index: var(--z-modal);
  width: min(560px, 100%);
  max-height: calc(100vh - var(--s-8));
  display: flex;
  flex-direction: column;
  background: var(--card);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-overlay);
  animation: modal-in var(--t-slow) var(--ease-out);
}

.modal--sm { width: min(420px, 100%); }
.modal--lg { width: min(820px, 100%); }

/*
 * Two short fields on one line.
 *
 * A form of eight stacked fields scrolls inside its own modal, and a field
 * below the fold is a field people leave empty — which for the short-course
 * form meant courses created with no run, and a catalogue nobody could join.
 * Widening the modal alone does not fix that: the fields are stacked, so the
 * scroll is a function of how many there are, not of how wide they are.
 *
 * auto-fit rather than a media query: the pair collapses when the column it
 * sits in is too narrow, which is the right answer inside a modal on a phone
 * and also inside anything narrower we have not thought of yet.
 */
.field-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0 var(--s-4);
}

.modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-5) var(--s-3);
}

.modal__title { font-family: var(--font-display); font-size: var(--t-xl); font-weight: var(--w-semibold); line-height: var(--lh-tight); }
.modal__body  { padding: 0 var(--s-5) var(--s-5); overflow-y: auto; }

.modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  border-top: 1px solid var(--line);
  background: var(--paper-raised);
  border-radius: 0 0 var(--r-xl) var(--r-xl);
}

.modal__close {
  flex-shrink: 0;
  padding: var(--s-1);
  background: none;
  border: none;
  color: var(--text-subtle);
  cursor: pointer;
  border-radius: var(--r-sm);
}
.modal__close:hover { background: var(--paper-sunken); color: var(--text); }

/* Confirmation dialogs state consequence, not just intent — the brief
   requires the user be told whether an action can be undone. */
.confirm__consequence {
  margin-top: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  font-size: var(--t-sm);
  background: var(--warning-tint);
  color: var(--ochre-deep);
}

.confirm__consequence--permanent { background: var(--danger-tint); color: var(--danger); }

@keyframes fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-out { to { opacity: 0; } }

@keyframes modal-in {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 560px) {
  .modal-backdrop { align-items: flex-end; padding: 0; }
  .modal {
    width: 100%;
    max-height: 92vh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
  }
  .modal__footer { border-radius: 0; }
  .modal__footer .btn { flex: 1; }
}

/* --- Loading states ----------------------------------------------------- */

.skeleton {
  background: linear-gradient(90deg, var(--paper-sunken) 25%, var(--paper-muted) 50%, var(--paper-sunken) 75%);
  background-size: 200% 100%;
  border-radius: var(--r-sm);
  animation: skeleton 1.4s var(--ease) infinite;
}

@keyframes skeleton {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* Applied to any container while its contents are being replaced by AJAX. */
.is-loading {
  position: relative;
  pointer-events: none;
  opacity: 0.55;
  transition: opacity var(--t-fast) var(--ease);
}

/* --- Layout primitives -------------------------------------------------- */

.stack   { display: flex; flex-direction: column; gap: var(--s-4); }
.row     { display: flex; align-items: center; gap: var(--s-3); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); }
.spacer  { flex: 1; }

.grid { display: grid; gap: var(--s-4); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

.container {
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 var(--s-5);
}

/* Screen-reader-only text: keeps announcements available to assistive tech
   without occupying layout. */
.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;
}

/* --- Empty states ------------------------------------------------------- */

.empty {
  padding: var(--s-12) var(--s-5);
  text-align: center;
}
.empty__title  { font-family: var(--font-display); font-size: var(--t-lg); font-weight: var(--w-semibold); margin-bottom: var(--s-2); }
.empty__body   { color: var(--text-muted); font-size: var(--t-base); max-width: 380px; margin: 0 auto var(--s-5); }

/* =========================================================================
 * Danger zone — the two operations on the settings screen that act on the
 * institute's data as a whole.
 *
 * Given its own border colour rather than dropped in among the settings
 * cards. The visual break is the point: everything above it is reversible by
 * typing the old value back in, and nothing here is.
 * ====================================================================== */

.danger-zone { border-color: var(--danger); }

.danger-zone .card__header {
  background: var(--danger-tint);
  border-bottom-color: var(--danger);
}

.danger-zone .card__header .heading { color: var(--danger); }

/* Stacks on a narrow screen, where a button beside a paragraph of prose
   leaves neither enough room. */
.danger-zone__row { align-items: flex-start; }

.danger-zone__row > div { flex: 1; min-width: 0; }

.danger-zone__row .btn { flex-shrink: 0; }

.danger-zone__title {
  font-weight: var(--w-semibold);
  margin-bottom: var(--s-2);
}

.danger-zone__row .muted + .muted { margin-top: var(--s-2); }

.danger-zone__rule {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--s-6) 0;
}

.danger-zone__blocked {
  margin-top: var(--s-2);
  padding: var(--s-3);
  border-radius: var(--r-sm);
  background: var(--danger-tint);
  color: var(--danger);
  font-size: var(--t-sm);
}

.danger-zone__blocked code {
  display: inline-block;
  margin-top: 4px;
  font-size: var(--t-sm);
}

/* A banner that carries a condition the page cannot fix by itself — storage
   credentials sealed under an APP_KEY this server no longer has, and anything
   of that shape later. Distinct from a toast because it is not about an action
   just taken; it is a state, and it should still be there after a reload. */
.callout {
  padding: var(--s-4);
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--paper-sunken);
  font-size: var(--t-sm);
  line-height: 1.55;
}

.callout code { font-size: var(--t-sm); }

.callout-danger {
  border-color: var(--danger);
  background: var(--danger-tint);
  color: var(--danger);
}

/* The identifier and password the seeder reports, shown once. Monospaced so
   a demonstrator reading it aloud does not confuse l for 1. */
.credentials {
  margin-top: var(--s-3);
  padding: var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--paper-sunken);
  font-family: var(--font-mono, monospace);
  font-size: var(--t-sm);
  word-break: break-all;
}

@media (max-width: 640px) {
  .danger-zone__row { flex-direction: column; }
  .danger-zone__row .btn { width: 100%; }
}

/* A ghost button for a destructive action. Same weight as its neighbours until
   it is reached for — a Delete that shouts from a row of six buttons is one
   people learn to tune out, and this sits beside Edit and Archive. */
.btn-danger-ghost { background: transparent; color: var(--text-muted); }
.btn-danger-ghost:hover:not(:disabled) { background: var(--danger-tint); color: var(--danger); }

/* A link that reads as text until it is pointed at. For table cells where the
   whole row is already visually busy and a line of blue would be noise. */
.link-plain { color: inherit; text-decoration: none; }
.link-plain:hover { color: var(--teal); text-decoration: underline; }
