/* Combyn design tokens and components.
   Hand-written: there is no Node toolchain here, and nothing loads from a CDN,
   so the app runs offline and the CSP can stay strict. */

:root {
  color-scheme: light dark;

  --bg:            #fbfbfd;
  --bg-raised:     #ffffff;
  --bg-sunken:     #f2f2f6;
  --bg-hover:      #eeeef4;

  --text:          #16161d;
  --text-muted:    #61616e;
  --text-faint:    #8c8c99;

  --border:        #e3e3ea;
  --border-strong: #cdcdd8;

  --accent:        #4f46e5;
  --accent-hover:  #4338ca;
  --accent-soft:   #eef2ff;
  --accent-text:   #ffffff;

  --danger:        #dc2626;
  --danger-soft:   #fef2f2;
  --success:       #059669;
  --success-soft:  #ecfdf5;
  --warning:       #b45309;
  --warning-soft:  #fffbeb;

  --radius:    10px;
  --radius-sm: 6px;
  --radius-lg: 16px;

  --shadow-sm: 0 1px 2px rgba(16, 16, 30, .06);
  --shadow:    0 4px 16px rgba(16, 16, 30, .08);
  --shadow-lg: 0 16px 48px rgba(16, 16, 30, .12);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --max-width: 1160px;

  /* Side padding for every full-width container. Narrowed on small screens in
     one place rather than per component. */
  --gutter: 24px;
  /* Minimum comfortable tap target. Apple and Google both land near 44px; the
     stylesheet only enforces it where the pointer is coarse, so a mouse-driven
     browser keeps its denser controls. */
  --tap: 44px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:            #0d0d12;
    --bg-raised:     #16161d;
    --bg-sunken:     #101017;
    --bg-hover:      #1f1f29;

    --text:          #ececf1;
    --text-muted:    #a0a0b0;
    --text-faint:    #70707f;

    --border:        #26262f;
    --border-strong: #363642;

    --accent:        #7c74f0;
    --accent-hover:  #918bf5;
    --accent-soft:   #1c1a35;

    --danger:        #f87171;
    --danger-soft:   #2a1416;
    --success:       #34d399;
    --success-soft:  #0d2620;
    --warning:       #fbbf24;
    --warning-soft:  #2a1f0d;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow:    0 4px 16px rgba(0, 0, 0, .45);
    --shadow-lg: 0 16px 48px rgba(0, 0, 0, .55);
  }
}

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  /* Stops iOS inflating body text when a phone is turned to landscape. */
  -webkit-text-size-adjust: 100%;
  /* Long identifiers -- an email, a target URI, a permission code -- must wrap
     rather than push the whole page sideways on a narrow screen. */
  overflow-wrap: break-word;
}

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 .5em; font-weight: 640; letter-spacing: -.015em; }
h1 { font-size: clamp(1.45rem, 4.6vw, 1.85rem); }
h2 { font-size: clamp(1.15rem, 3vw, 1.3rem); }
h3 { font-size: 1.05rem; }
p  { margin: 0 0 1em; }
a  { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code, .mono { font-family: var(--mono); font-size: .85em; }

.shell {
  max-width: var(--max-width); margin: 0 auto;
  /* max() keeps the content clear of a notch or a rounded corner in landscape,
     where the safe-area inset is wider than the gutter. */
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}
.stack   > * + * { margin-top: 16px; }
.stack-l > * + * { margin-top: 32px; }
.muted { color: var(--text-muted); }
.faint { color: var(--text-faint); }
.small { font-size: .875rem; }
.center { text-align: center; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.row-between { display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.grow { flex: 1; }

/* --- Admin hub ------------------------------------------------------------- */

.admin-card { display: flex; flex-direction: column; gap: 8px; text-decoration: none; }
.admin-card:hover { text-decoration: none; border-color: var(--border-strong); }
.admin-card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.admin-card-name { font-weight: 620; color: var(--text); }
/* The number reads first and the caption second, so a glance down the column
   compares like with like. */
.admin-card-stat {
  font-size: 1.35rem; font-weight: 640; color: var(--text);
  display: flex; align-items: baseline; gap: 6px;
}
.admin-card-stat .faint { font-weight: 400; }
.admin-card-note { margin-top: 2px; }

.attn-list { list-style: none; margin: 0; padding: 0; }
.attn {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px var(--gutter); border-bottom: 1px solid var(--border);
  font-size: .9rem;
}
.attn:last-child { border-bottom: none; }
.attn-text { flex: 1; min-width: 0; }
.attn-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.attn-warn .attn-dot { background: var(--warning); }
.attn-info .attn-dot { background: var(--text-faint); }

/* --- Trying on a role ------------------------------------------------------ */

/* Above the top bar and sticky with it, so the fact that you are in this mode
   cannot scroll away. Amber rather than red: this is a state, not a fault. */
.simbar {
  position: sticky; top: 0; z-index: 60;
  background: var(--warning); color: #fff;
  border-bottom: 1px solid color-mix(in srgb, var(--warning) 70%, black);
}
.simbar-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 8px 20px; flex-wrap: wrap;
}
.simbar-text { font-size: .88rem; }
.simbar .btn { background: #fff; color: var(--warning); border-color: #fff; }

/* The bar takes the sticky top slot, so the top bar has to sit below it rather
   than under it -- both are sticky and both would otherwise claim y=0. */
.simbar ~ .topbar { top: 37px; }

.sim-roles { display: flex; flex-wrap: wrap; gap: 12px; }
.sim-role {
  border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
  display: flex; flex-direction: column; gap: 6px; min-width: 150px;
}
.sim-role-name { font-weight: 600; }

/* A matrix with one column per role has no sensible stacked form on a phone,
   so it scrolls sideways instead of being crushed. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.sim-matrix th[scope="row"] { text-align: left; font-weight: 500; white-space: nowrap; }
.sim-matrix th[scope="row"] code { display: block; font-weight: 400; }
.sim-cell { text-align: center; }
.sim-yes { color: var(--success); font-weight: 700; }
.sim-no { color: var(--text-faint); }

/* --- Top bar --------------------------------------------------------------- */

.topbar {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
  /* On the console, `body` is a flex column and this is one of its items. An
     item that fails to stretch is sized to its content instead -- which showed
     up as a bar exactly as wide as the brand plus the menu button, with the
     rest of the row bare and the button stranded mid-screen. In a column flex
     container `width: 100%` is the same instruction as stretching, said in a
     way that does not depend on it; in normal flow it is what a block element
     does anyway, so it costs every other page nothing. */
  width: 100%;
}
.topbar-inner { display: flex; align-items: center; gap: 28px; height: 60px; }

.brand { display: inline-flex; align-items: center; gap: 9px; font-weight: 680; font-size: 1.05rem; color: var(--text); letter-spacing: -.02em; }
.brand:hover { text-decoration: none; }
.brand-mark {
  width: 26px; height: 26px; border-radius: 8px; flex: none;
  background: linear-gradient(140deg, var(--accent), #a855f7);
  display: grid; place-items: center;
  color: #fff; font-size: .8rem; font-weight: 700;
}

.nav { display: flex; gap: 4px; align-items: center; }
.nav a {
  padding: 6px 11px; border-radius: var(--radius-sm);
  color: var(--text-muted); font-weight: 520; font-size: .9rem;
}
.nav a:hover { background: var(--bg-hover); color: var(--text); text-decoration: none; }
.nav a.active { background: var(--accent-soft); color: var(--accent); }

/* --- Phone navigation -------------------------------------------------------
   Six tabs, a name and a sign-out button do not fit across a phone. Below the
   breakpoint the tabs and the account block are replaced by one <details>
   drawer -- no script, so it works under the strict CSP and with JS off. */

.navdrop { display: none; margin-left: auto; }
.navdrop-toggle {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: var(--tap); padding: 6px 12px 6px 10px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg-raised); color: var(--text);
  font-size: .9rem; font-weight: 560; cursor: pointer;
  list-style: none;              /* Firefox */
}
.navdrop-toggle::-webkit-details-marker { display: none; }   /* Safari */
.navdrop-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.navdrop[open] .navdrop-toggle { background: var(--bg-hover); border-color: var(--border-strong); }

.navdrop-icon { display: grid; gap: 3px; flex: none; }
.navdrop-icon i { display: block; width: 16px; height: 2px; border-radius: 1px; background: var(--text-muted); }
.navdrop[open] .navdrop-icon i { background: var(--accent); }
.navdrop-here { max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.navdrop-panel {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--bg-raised); border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  padding: 8px max(var(--gutter), env(safe-area-inset-right)) 12px
           max(var(--gutter), env(safe-area-inset-left));
  /* A long menu scrolls inside itself instead of running off the screen.
     --app-height, where console.js has measured it, so an open keyboard cannot
     leave the menu taller than what is visible. */
  max-height: calc(var(--app-height, 100dvh) - 56px);
  overflow-y: auto; overscroll-behavior: contain;
}

.navdrop-links { display: grid; gap: 2px; }
.navdrop-links a {
  display: flex; align-items: center; min-height: var(--tap);
  padding: 10px 12px; border-radius: var(--radius-sm);
  color: var(--text); font-weight: 540;
}
.navdrop-links a:hover { background: var(--bg-hover); text-decoration: none; }
.navdrop-links a.active { background: var(--accent-soft); color: var(--accent); }

.navdrop-app {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--border);
}
/* Not `navdrop-ios`: the guard in tests/test_console.py matches class names on
   word boundaries, so anything starting `navdrop` is read as `.navdrop` itself
   -- which does set `display`, and is therefore a name worth staying clear of
   on an element that carries `hidden`. */
.app-hint { margin: 6px 0 0; }

.navdrop-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--border);
}
.navdrop-user { display: grid; color: var(--text); font-weight: 540; }
.navdrop-user:hover { text-decoration: none; }

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

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 16px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: var(--bg-raised);
  color: var(--text); font: inherit; font-size: .9rem; font-weight: 560;
  cursor: pointer; transition: background .12s, border-color .12s, transform .06s;
  text-decoration: none; white-space: nowrap;
}
/* `display` here would otherwise beat the browser's own [hidden] rule -- same
   specificity, and this stylesheet wins -- leaving a button that JavaScript
   hid still sitting on the page. */
.btn[hidden] { display: none; }
.btn:hover { background: var(--bg-hover); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
.btn-danger:hover { background: var(--danger-soft); }

.btn-ghost { background: transparent; border-color: transparent; color: var(--text-muted); }
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }

.btn-sm { padding: 5px 11px; font-size: .82rem; }
.btn-lg { padding: 12px 24px; font-size: .98rem; }
.btn-block { width: 100%; }

/* --- Cards & panels -------------------------------------------------------- */

.card {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.card-pad { padding: 22px; }
.card-head {
  padding: 16px 22px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.card-head h2, .card-head h3 { margin: 0; }
.card-body { padding: 22px; }
.card-foot { padding: 16px 22px; border-top: 1px solid var(--border); background: var(--bg-sunken); border-radius: 0 0 var(--radius) var(--radius); }

.grid { display: grid; gap: 18px; }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }

.stat { padding: 20px 22px; }
.stat-value { font-size: 2rem; font-weight: 680; letter-spacing: -.03em; line-height: 1.1; }
.stat-label { color: var(--text-muted); font-size: .85rem; margin-top: 4px; }

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

.field { display: block; margin-bottom: 16px; }
.field > label, .label {
  display: block; font-size: .85rem; font-weight: 580;
  margin-bottom: 6px; color: var(--text);
}
.hint { font-size: .8rem; color: var(--text-faint); margin-top: 5px; }

input[type=text], input[type=email], input[type=password], input[type=search],
textarea, select {
  width: 100%; padding: 10px 12px; font: inherit; font-size: .92rem;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  transition: border-color .12s, box-shadow .12s;
}
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
input::placeholder { color: var(--text-faint); }

/* --- Alerts ---------------------------------------------------------------- */

.alert {
  padding: 12px 16px; border-radius: var(--radius-sm); font-size: .9rem;
  border: 1px solid transparent; margin-bottom: 18px;
}
.alert-error   { background: var(--danger-soft);  border-color: color-mix(in srgb, var(--danger) 30%, transparent);  color: var(--danger); }
.alert-success { background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 30%, transparent); color: var(--success); }
.alert-warning { background: var(--warning-soft); border-color: color-mix(in srgb, var(--warning) 30%, transparent); color: var(--warning); }
.alert-info    { background: var(--accent-soft);  border-color: color-mix(in srgb, var(--accent) 25%, transparent);  color: var(--accent); }

/* --- Badges & pills -------------------------------------------------------- */

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: 999px;
  font-size: .76rem; font-weight: 580; line-height: 1.7;
  background: var(--bg-sunken); color: var(--text-muted);
  border: 1px solid var(--border);
}
.badge-accent  { background: var(--accent-soft);  color: var(--accent);  border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
.badge-success { background: var(--success-soft); color: var(--success); border-color: color-mix(in srgb, var(--success) 25%, transparent); }
.badge-danger  { background: var(--danger-soft);  color: var(--danger);  border-color: color-mix(in srgb, var(--danger) 25%, transparent); }
.badge-mono { font-family: var(--mono); font-size: .72rem; }

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

.table-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: .9rem; }
table.data th {
  text-align: left; padding: 10px 16px; font-size: .76rem;
  text-transform: uppercase; letter-spacing: .05em; font-weight: 620;
  color: var(--text-faint); border-bottom: 1px solid var(--border);
  white-space: nowrap; background: var(--bg-sunken);
}
table.data td { padding: 12px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.data tbody tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--bg-hover); }
table.data td.tight { white-space: nowrap; }

/* --- Permission matrix ----------------------------------------------------- */

.perm-group + .perm-group { margin-top: 26px; }
.perm-group-title {
  font-size: .76rem; text-transform: uppercase; letter-spacing: .06em;
  font-weight: 640; color: var(--text-faint); margin-bottom: 10px;
}
.perm-list { display: grid; gap: 8px; }
.perm {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 14px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--bg);
  cursor: pointer; transition: border-color .12s, background .12s;
}
.perm:hover { border-color: var(--border-strong); background: var(--bg-hover); }
.perm input { margin: 3px 0 0; width: 16px; height: 16px; accent-color: var(--accent); flex: none; cursor: pointer; }
.perm:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: var(--accent-soft); }
.perm-name { font-weight: 560; font-size: .9rem; }
.perm-code { font-family: var(--mono); font-size: .72rem; color: var(--text-faint); }
.perm-desc { font-size: .82rem; color: var(--text-muted); margin-top: 2px; }

/* --- Page furniture -------------------------------------------------------- */

.page { padding: 34px 0 72px; }
.page-head { margin-bottom: 26px; }
.page-head h1 { margin-bottom: 4px; }
.breadcrumb { font-size: .84rem; color: var(--text-faint); margin-bottom: 8px; }
.breadcrumb a { color: var(--text-muted); }

.empty { padding: 56px 24px; text-align: center; color: var(--text-muted); }
.empty-title { font-weight: 600; color: var(--text); margin-bottom: 4px; }

.pagination { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 24px; }

.divider { height: 1px; background: var(--border); border: 0; margin: 28px 0; }

/* --- Auth screens ---------------------------------------------------------- */

.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px 20px; }
/* dvh, not vh: on a phone vh includes the space under the address bar, which
   pushes the sign-in card down and leaves the page scrollable for no reason. */
@supports (height: 100dvh) { .auth-wrap { min-height: 100dvh; } }
.auth-card { width: 100%; max-width: 410px; }
.auth-head { text-align: center; margin-bottom: 26px; }
.auth-head .brand { justify-content: center; font-size: 1.2rem; margin-bottom: 16px; }
.auth-foot { text-align: center; margin-top: 20px; font-size: .88rem; color: var(--text-muted); }

/* "or", with a rule either side. The lines are the flex children growing into
   whatever the word leaves, so they stay even as the card resizes. */
.auth-or { display: flex; align-items: center; gap: 12px; margin: 18px 0; }
.auth-or::before, .auth-or::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}
.auth-or span { font-size: .82rem; color: var(--text-muted); }

/* Microsoft's own guidance: their colours, not ours, so the button is
   recognisable as theirs at a glance. */
.btn-microsoft {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--bg-raised); color: var(--text);
  border: 1px solid var(--border-strong); text-decoration: none;
}
.btn-microsoft:hover { background: var(--bg-hover); text-decoration: none; }

/* The four squares, drawn rather than fetched: an <img> would be one more
   request, and an inline SVG would need a CSP exception. */
.ms-logo {
  display: grid; grid-template-columns: 9px 9px; grid-template-rows: 9px 9px;
  gap: 2px; flex: none;
}
.ms-logo i { display: block; }
.ms-logo i:nth-child(1) { background: #f25022; }
.ms-logo i:nth-child(2) { background: #7fba00; }
.ms-logo i:nth-child(3) { background: #00a4ef; }
.ms-logo i:nth-child(4) { background: #ffb900; }

/* --- Landing page ---------------------------------------------------------- */

.hero { padding: 96px 0 80px; text-align: center; position: relative; overflow: hidden; }
.hero::before {
  content: ""; position: absolute; inset: -40% 20% auto; height: 620px;
  background: radial-gradient(ellipse at center,
              color-mix(in srgb, var(--accent) 20%, transparent), transparent 62%);
  pointer-events: none; z-index: 0;
}
.hero > * { position: relative; z-index: 1; }
.hero h1 {
  font-size: clamp(2.3rem, 5.5vw, 3.9rem); letter-spacing: -.035em;
  max-width: 16ch; margin: 0 auto .55em; font-weight: 700;
}
.hero .lede {
  font-size: clamp(1.02rem, 2vw, 1.2rem); color: var(--text-muted);
  max-width: 56ch; margin: 0 auto 34px;
}
.eyebrow {
  display: inline-flex; align-items: center; gap: 8px; margin-bottom: 24px;
  padding: 5px 14px; border-radius: 999px; font-size: .8rem; font-weight: 560;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
}

.section { padding: 76px 0; }
.section-head { text-align: center; max-width: 60ch; margin: 0 auto 48px; }
.section-head h2 { font-size: clamp(1.6rem, 3.2vw, 2.2rem); letter-spacing: -.025em; }
.section-head p { color: var(--text-muted); font-size: 1.02rem; margin: 0; }

.feature { padding: 26px; }
.feature-icon {
  width: 38px; height: 38px; border-radius: 10px; margin-bottom: 14px;
  display: grid; place-items: center; font-size: 1.1rem;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
}
.feature h3 { font-size: 1rem; margin-bottom: 6px; }
.feature p { color: var(--text-muted); font-size: .9rem; margin: 0; }

.tier { padding: 24px; display: flex; gap: 16px; align-items: flex-start; }
.tier-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 8px; flex: none; background: var(--accent); }
.tier h3 { margin-bottom: 2px; font-size: .98rem; }
.tier p { margin: 0; font-size: .875rem; color: var(--text-muted); }

.cta-band {
  padding: 64px 40px; border-radius: var(--radius-lg); text-align: center;
  background: linear-gradient(135deg,
              color-mix(in srgb, var(--accent) 12%, var(--bg-raised)),
              var(--bg-raised));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
}
.cta-band h2 { font-size: clamp(1.5rem, 3vw, 2rem); letter-spacing: -.025em; }

.site-foot {
  border-top: 1px solid var(--border); padding: 30px 0;
  color: var(--text-faint); font-size: .85rem;
}

/* --- Utilities ------------------------------------------------------------- */

.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;
}

/* The small-screen and touch rules live at the end of this file. They override
   components rather than introduce them, and a media query adds no specificity
   -- so they only win by coming last. */

.center-row { justify-content: center; }
.section-alt { background: var(--bg-sunken); border-block: 1px solid var(--border); }
.h2-size { font-size: 1.5rem; margin-bottom: .25em; }
.no-margin { margin: 0; }
.truncate { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roadmap-row { padding: 10px 0; border-bottom: 1px solid var(--border); }
.roadmap-row:last-child { border-bottom: none; }
a.card.stat { color: inherit; display: block; transition: border-color .12s, box-shadow .12s; }
a.card.stat:hover { text-decoration: none; border-color: var(--accent); box-shadow: var(--shadow); }
.person-name { font-weight: 560; }
.search-row input[type=search] { max-width: 340px; }

/* --- homepage editor ------------------------------------------------------- */

.env-card .card-head h2 { font-size: 1.08rem; }
.env-live .card-head { border-bottom-color: color-mix(in srgb, var(--success) 35%, var(--border)); }

.diff-list { list-style: none; margin: 0; padding: 0; font-size: .86rem; }
.diff-list > li { padding: 10px 0; border-bottom: 1px solid var(--border); }
.diff-list > li:last-child { border-bottom: none; }
.diff-kind {
  display: inline-block; min-width: 62px; padding: 1px 8px; margin-right: 8px;
  border-radius: 999px; font-size: .7rem; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  background: var(--bg-sunken); color: var(--text-muted);
}
.diff-added   .diff-kind { background: var(--success-soft); color: var(--success); }
.diff-removed .diff-kind { background: var(--danger-soft);  color: var(--danger); }
.diff-changed .diff-kind { background: var(--accent-soft);  color: var(--accent); }
.diff-moved   .diff-kind { background: var(--warning-soft); color: var(--warning); }
.diff-what { font-weight: 580; }
.diff-fields { list-style: none; margin: 6px 0 0 70px; padding: 0; }
.diff-fields li { padding: 3px 0; display: flex; gap: 8px; flex-wrap: wrap; align-items: baseline; }
.diff-before { color: var(--danger); text-decoration: line-through; opacity: .8; }
.diff-after  { color: var(--success); }

.item-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 18px; background: var(--bg); }
.item-head { margin-bottom: 14px; }
.danger-text { color: var(--danger); }
.grow-select { flex: 1; min-width: 240px; }
select { cursor: pointer; }

/* --- The vault --------------------------------------------------------------
   A note is a document, so it is set like one: measure capped for reading,
   headings that step down clearly, and links to notes that do not exist yet
   marked as the invitations they are rather than hidden as mistakes. */

.note-index { list-style: none; margin: 0; padding: 0; }
.note-index > li {
  padding: 14px 22px;
  border-bottom: 1px solid var(--border);
}
.note-index > li:last-child { border-bottom: none; }
.note-index a { display: block; color: inherit; }
.note-index a:hover { text-decoration: none; }
.note-index a:hover .note-index-title { color: var(--accent); }
.note-index-title { display: block; font-weight: 580; }
.note-index-excerpt {
  display: block; color: var(--text-muted); font-size: .875rem;
  margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.note-index-meta {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 6px;
}

.note-tags { margin-top: 10px; }

.note-body { max-width: 72ch; }
.note-body > :first-child { margin-top: 0; }
.note-body h2 { font-size: 1.2rem; margin: 1.6em 0 .5em; }
.note-body h3 { font-size: 1.02rem; margin: 1.4em 0 .4em; }
.note-body h4, .note-body h5 { font-size: .95rem; margin: 1.2em 0 .3em; }
.note-body p { margin: 0 0 1em; }
.note-list { margin: 0 0 1em; padding-left: 1.3em; }
.note-list li { margin: .25em 0; }
.note-body blockquote {
  margin: 0 0 1em; padding: .4em 0 .4em 1em;
  border-left: 3px solid var(--border-strong); color: var(--text-muted);
}
.note-body code {
  font-family: var(--mono); font-size: .875em;
  background: var(--bg-sunken); padding: .1em .35em; border-radius: var(--radius-sm);
}
.note-code {
  font-family: var(--mono); font-size: .8rem;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .7rem .8rem; margin: 0 0 1em;
  overflow-x: auto;
}
.note-code code { background: none; padding: 0; }

.note-link { font-weight: 540; }
/* Dashed underline: it points somewhere real, but somewhere unwritten. */
.note-link-new {
  color: var(--text-muted);
  text-decoration: underline dashed;
  text-underline-offset: 3px;
}
.note-link-new:hover { color: var(--accent); }

.note-editor { font-family: var(--mono); font-size: .85rem; line-height: 1.6; }

/* Version list: the markers sit under what changed, not beside it, so a long
   commit subject does not squeeze them off the row on a phone. */
.version-marks:empty { display: none; }
.version-marks { gap: 6px; margin-top: 4px; }

/* --- Master console -------------------------------------------------------
   A transcript, not a chat app: the timeline mixes what was asked, what the
   agent did, and what it answered, so activity rows are deliberately quieter
   than messages rather than being hidden behind a toggle. */

/* The console is the one screen that behaves like an application rather than a
   document: the transcript scrolls inside its own region and the composer stays
   put, so the thing you type into never walks off the bottom of a long history.
   Scoped to this page by a body class -- every other page scrolls normally. */
body.app-shell {
  /* Three heights, worst to best.
     `vh` is wrong on a phone: it counts the space behind the address bar.
     `dvh` fixes that but still ignores the on-screen keyboard -- iOS shrinks
     only the *visual* viewport when it opens, so a shell sized to the layout
     viewport keeps its composer underneath the keys, invisible while you type
     into it. `--app-height` is the visual viewport, measured by console.js and
     kept current as the keyboard comes and goes. */
  height: 100vh;
  height: 100dvh;
  height: var(--app-height, 100dvh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* No pull-to-refresh: a downward swipe on a transcript is how you read it,
     not how you ask for the page to be thrown away and fetched again. */
  overscroll-behavior: none;
}
/* Both children say their own width, rather than relying on the flex container
   to stretch them. The top bar was coming out as wide as its contents and no
   wider -- brand, menu button, then bare background -- which is what a flex
   item that has not stretched looks like. Three ways of saying the same thing,
   because only one of them has to survive. */
body.app-shell { align-items: stretch; }
body.app-shell > .topbar,
body.app-shell > main { align-self: stretch; width: 100%; }

body.app-shell main {
  flex: 1;
  min-height: 0;           /* without this the log grows instead of scrolling */
  min-width: 0;            /* and see below */
  display: flex;
}
body.app-shell .console-shell {
  flex: 1;
  /* Both minimums, and the horizontal one is not decoration.
     A flex item's automatic minimum size is its *content's* minimum -- it will
     refuse to shrink below the widest thing inside it. `main` lays out in a
     row, so that minimum is a width, and one long unbreakable token anywhere in
     the transcript makes this shell wider than the screen. The page then
     overflows, Chrome shrink-to-fits the whole document to show the overflow,
     and everything reads at the wrong scale with the top bar apparently ending
     two-thirds of the way across. The bar was never the problem; it was the
     only element honest about how wide the page had become. */
  min-height: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* A page agent's standing context: where the site is, one tap from the chat.
   The agent reads the same state, so the strip and the answer cannot disagree. */
.agent-state {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
  padding: 10px 0 0;
  font-size: .85rem;
}
.agent-state-link {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--text); font-weight: 540;
}
.agent-state-link:hover { text-decoration: none; color: var(--accent); }

.console-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 1.5rem 0 1rem;
}

.console-title { margin: 0 0 .25rem; font-size: 1.4rem; }

.con-presence {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .4rem .75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-raised);
  font-size: .85rem;
  white-space: nowrap;
}

.con-dot {
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  background: var(--text-faint);
  flex: none;
}
.con-presence[data-status="idle"]    .con-dot { background: var(--success); }
.con-presence[data-status="working"] .con-dot {
  background: var(--accent);
  animation: con-pulse 1.4s ease-in-out infinite;
}
.con-presence[data-status="working"] { border-color: var(--accent); }

@keyframes con-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.8); }
}

.con-presence-note { color: var(--text-muted); }

/* --- the timeline --------------------------------------------------------- */

.console-log {
  list-style: none;
  margin: 0;
  padding: 0 0 .5rem;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Not decoration: a box with `overflow-y: auto` and `overflow-x: visible`
     computes the visible axis to `auto` as well, so the log would quietly
     become sideways-scrollable the moment anything inside it was too wide --
     and then everything reads shifted, with the start of every line cut off. */
  overflow-x: hidden;
  overscroll-behavior: contain;   /* scrolling the log never scrolls the page */
}

.con-empty { padding: 2rem 0; }

.con-msg {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
  padding: .75rem .9rem;
  max-width: 54rem;
  /* A flex item refuses to shrink below its content by default. Without this a
     single long word -- a path, a URL, a token -- sets the width of the whole
     transcript and pushes the rest of the page off the side of the screen. */
  min-width: 0;
}

.con-user {
  align-self: flex-end;
  background: var(--accent-soft);
  border-color: transparent;
}

.con-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .35rem;
}

.con-who { font-weight: 600; font-size: .8rem; }
.con-time { color: var(--text-faint); font-size: .75rem; font-variant-numeric: tabular-nums; }

.con-body { line-height: 1.55; overflow-wrap: anywhere; }
.con-body code {
  font-family: var(--mono);
  font-size: .875em;
  background: var(--bg-sunken);
  padding: .1em .35em;
  border-radius: var(--radius-sm);
}

.console-code {
  font-family: var(--mono);
  font-size: .8rem;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .6rem .75rem;
  margin: .5rem 0;
  overflow-x: auto;   /* long commands scroll here, not the whole page */
}
.console-code code { background: none; padding: 0; }

/* Activity: one line per tool call, sized to be skimmed past. */
.con-act {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
  padding: .1rem .2rem;
  font-size: .82rem;
  color: var(--text-muted);
}

.con-act-dot {
  width: .35rem;
  height: .35rem;
  border-radius: 50%;
  background: var(--border-strong);
  flex: none;
  transform: translateY(-.15em);
}

.con-act-tool {
  font-family: var(--mono);
  font-size: .75rem;
  color: var(--text-faint);
  background: var(--bg-sunken);
  border-radius: var(--radius-sm);
  padding: .05rem .4rem;
}

.con-act-body { overflow-wrap: anywhere; }

.con-act-more { flex-basis: 100%; margin-left: 1.4rem; }
.con-act-more summary { cursor: pointer; color: var(--text-faint); font-size: .75rem; }
.con-act-more pre {
  font-family: var(--mono);
  font-size: .75rem;
  background: var(--bg-sunken);
  border-radius: var(--radius-sm);
  padding: .5rem .6rem;
  margin: .35rem 0 0;
  overflow-x: auto;
  white-space: pre-wrap;
}

/* --- working indicator ---------------------------------------------------- */

/* `display` here would otherwise beat the browser's own [hidden] rule -- same
   specificity, and this stylesheet wins -- leaving the dots animating and the
   word "Working" on screen while nothing is running. */
.con-working[hidden] { display: none; }

.con-working {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex: none;
  margin: 0;
  padding: .5rem .75rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
  font-size: .85rem;
  color: var(--text);
}

.con-working-tool {
  font-family: var(--mono);
  font-size: .75rem;
  color: var(--text-muted);
  background: var(--bg-raised);
  border-radius: var(--radius-sm);
  padding: .05rem .4rem;
  flex: none;
}

.con-working-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.con-working-dots { display: inline-flex; gap: .2rem; }
.con-working-dots { flex: none; align-self: center; }
.con-working-dots i {
  width: .3rem;
  height: .3rem;
  border-radius: 50%;
  background: var(--accent);
  animation: con-bounce 1.2s ease-in-out infinite;
}
.con-working-dots i:nth-child(2) { animation-delay: .15s; }
.con-working-dots i:nth-child(3) { animation-delay: .3s; }

@keyframes con-bounce {
  0%, 60%, 100% { opacity: .25; }
  30%           { opacity: 1; }
}

.con-working-for { color: var(--text-faint); font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .con-presence[data-status="working"] .con-dot,
  .con-working-dots i { animation: none; }
}

/* --- composer ------------------------------------------------------------- */

.console-composer {
  flex: none;
  margin: 0;
  padding: .75rem 0 1rem;
  border-top: 1px solid var(--border);
  background: var(--bg);
}

.console-composer textarea {
  width: 100%;
  resize: vertical;
  font: inherit;
  padding: .7rem .85rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  /* The floor for the auto-growing box in console.js, which only ever adds to
     it. Roughly the three rows the textarea would have without any script. */
  min-height: 6rem;
}
.console-composer textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.console-composer-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;      /* onto a second line before off the side of the screen */
  gap: .6rem;
  margin-top: .5rem;
}

/* --- attachments ----------------------------------------------------------
   A picture is often the whole message -- "look at this" needs no caption --
   so attached images are shown at a size worth looking at rather than as a
   paperclip you have to decide to open. */

.con-attach { flex: none; gap: 6px; }
.con-clip { font-size: .95em; line-height: 1; }
.con-dropping { outline: 2px dashed var(--accent); outline-offset: 4px; }

/* What is about to be sent. Hidden until there is something in it -- the rule
   below beats the browser's own [hidden] handling, which this would lose to. */
.con-picks[hidden] { display: none; }
.con-picks {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  list-style: none;
  margin: 0 0 .5rem;
  padding: 0;
}

.con-pick { position: relative; flex: none; }
.con-pick img {
  display: block;
  width: 64px;
  height: 64px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-sunken);
}
.con-pick-drop {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  font-size: .9rem;
  line-height: 1;
  cursor: pointer;
}
.con-pick-drop:hover { background: var(--danger-soft); color: var(--danger); }

/* Attached images inside a message on the timeline. */
.con-shots {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  list-style: none;
  margin: .5rem 0 0;
  padding: 0;
}
.con-shots li { flex: none; }
.con-shots img {
  display: block;
  max-width: 100%;
  /* Tall phone screenshots are the common case: cap the height, not the width,
     or one of them fills the transcript on its own. */
  max-height: 320px;
  width: auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-sunken);
}
.con-shot-file {
  display: inline-block;
  padding: .35rem .6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: .82rem;
}

/* --- Small screens and touch ------------------------------------------------
   The panel is run from a phone as much as from a desk, so what follows is not
   a fallback: it is the layout half the time. Three things drive it --
   reachable tap targets, no horizontal scrolling, and tables that read as cards
   rather than as a spreadsheet seen through a letterbox.

   Everything responsive lives here, at the end, because a media query adds no
   specificity: a rule that ties with a component's own rule wins only by being
   declared after it. */

/* The tabs give way to the drawer while there is still room to spare, so the
   bar never reaches the point of wrapping onto two lines. */
@media (max-width: 900px) {
  .nav, .topbar-account { display: none; }
  .navdrop { display: block; }
  .topbar-inner { gap: 12px; height: 56px; }
}

@media (max-width: 720px) {
  :root { --gutter: 16px; }

  .page { padding: 20px 0 56px; }
  .page-head { margin-bottom: 18px; }
  .stack-l > * + * { margin-top: 22px; }

  /* Cards run to the edge of the gutter, so their own padding can be smaller. */
  .card-pad, .card-body { padding: 16px; }
  .card-head { padding: 14px 16px; flex-wrap: wrap; }
  .card-foot { padding: 14px 16px; }
  .grid { gap: 14px; }

  .stat { padding: 16px; }
  .stat-value { font-size: 1.7rem; }

  /* A heading and its actions stack rather than squeeze against each other.
     Both spellings occur: the row inside the head, and the head that is the
     row itself. */
  .row-between { align-items: flex-start; }
  .page-head .row-between,
  .row-between.page-head { flex-direction: column; gap: 12px; }

  .search-row { gap: 8px; }
  .search-row input[type=search] { max-width: none; flex: 1 1 100%; }

  .truncate { max-width: none; white-space: normal; overflow-wrap: anywhere; }
  .grow-select { min-width: 0; flex: 1 1 100%; }

  .empty { padding: 36px 16px; }
  .pagination { gap: 10px; }
  .divider { margin: 20px 0; }

  .hero { padding: 56px 0 44px; }
  .section { padding: 44px 0; }
  .section-head { margin-bottom: 32px; }
  .cta-band { padding: 36px 20px; }
  .feature, .tier { padding: 18px; }
  .site-foot { padding: 22px 0; }

  /* Sitting the card a little above centre keeps it clear of the keyboard. */
  .auth-wrap { padding: 8vh 16px 24px; align-content: start; }

  /* --- tables become cards --------------------------------------------------
     A six-column table on a 390px screen is a horizontal scrollbar with the
     data hidden inside it. Rows marked `stack-mobile` unroll into labelled
     blocks: the same cells, top to bottom, each captioned by its header. */
  table.data.stack-mobile thead { display: none; }
  table.data.stack-mobile,
  table.data.stack-mobile tbody,
  table.data.stack-mobile tr,
  table.data.stack-mobile td { display: block; width: auto; }

  table.data.stack-mobile tr {
    padding: 14px var(--gutter);
    border-bottom: 1px solid var(--border);
  }
  table.data.stack-mobile tbody tr:last-child { border-bottom: none; }

  table.data.stack-mobile td,
  table.data.stack-mobile td.tight {
    padding: 0; border: none;
    white-space: normal; overflow-wrap: anywhere;
  }
  table.data.stack-mobile td + td { margin-top: 9px; }

  /* A row header (the permission matrix uses one) becomes the block's title
     rather than staying a cell beside cells that are no longer cells. */
  table.data.stack-mobile th[scope="row"] {
    display: block; width: auto; padding: 0 0 9px; border: none;
  }

  table.data.stack-mobile td[data-label]::before {
    content: attr(data-label);
    display: block; margin-bottom: 1px;
    font-size: .7rem; font-weight: 620; letter-spacing: .05em;
    text-transform: uppercase; color: var(--text-faint);
  }
  /* Cells holding nothing but buttons read better with room above them. */
  table.data.stack-mobile td.actions { margin-top: 12px; }
  table.data.stack-mobile td.actions .row { gap: 8px; }

  /* A leading number (the section list, the revision list) belongs beside its
     title, not stacked above it on a line of its own. Flex rather than
     inline-block: whitespace between cells would otherwise count as width and
     push the pair onto two lines. */
  table.data.stack-mobile tr:has(> td.row-index) { display: flex; flex-wrap: wrap; }
  table.data.stack-mobile tr:has(> td.row-index) > td { flex: 1 1 100%; }
  table.data.stack-mobile td.row-index { flex: 0 0 2.2rem; }
  table.data.stack-mobile td.row-index + td { flex: 1 1 calc(100% - 2.2rem); margin-top: 0; }

  /* Key/value tables (Account, Details) keep two columns but lose the padding. */
  table.data th, table.data td { padding: 10px 12px; }

  /* 70px of indent on a 358px card leaves no room for what changed. */
  .diff-fields { margin-left: 0; }
  .diff-fields li { gap: 4px; }

  /* --- the console on a phone ----------------------------------------------
     Everything here buys height for the transcript, which is the only part of
     this screen anybody came to read. */

  /* Title and presence share one line instead of stacking into a banner. */
  .console-top {
    margin: 10px 0 8px;
    gap: .5rem;
    align-items: center;
    flex-wrap: nowrap;
  }
  /* min-width:0 lets the title shrink; without it a flex item refuses to go
     below its content and pushes the presence pill off the screen. */
  .console-top > div { min-width: 0; }
  .console-title {
    font-size: 1.1rem; margin: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .con-presence { flex: none; padding: .25rem .55rem; font-size: .78rem; }
  /* The note ("Bash: run the tests") repeats in the working indicator below. */
  .con-presence-note { display: none; }
  /* The standing explanation of what the console is costs three lines of
     transcript on every single visit, to say something you read once. It stays
     on the wider layout, where the room is free. */
  .console-top p { display: none; }

  .con-msg { max-width: 100%; padding: .65rem .75rem; }
  .con-user { max-width: 92%; }
  .console-log { gap: .5rem; }
  .con-act-more { margin-left: 0; }

  /* The composer stays inside the gutter. It was full-bleed for a moment, via
     negative margins meant to cancel the shell's padding exactly -- but that
     padding is `max(gutter, safe-area-inset)`, so on a screen with an inset the
     cancellation is wrong by the difference and the bar hangs over the edge.
     A prettier divider is not worth a page that can scroll sideways. */
  .console-composer {
    padding: .5rem 0 max(.5rem, env(safe-area-inset-bottom));
  }
  .console-composer textarea { padding: .55rem .7rem; min-height: 4.4rem; }
  .console-composer-actions { gap: .5rem; margin-top: .4rem; }

  /* The word goes, the paperclip stays: the button is still a 44px target,
     and the row has to hold the hint and Send as well. */
  .con-attach-label { display: none; }
  .con-attach { padding: 8px 12px; }
  .con-pick img { width: 56px; height: 56px; }
  /* Room for a tall screenshot without it swallowing the whole transcript. */
  .con-shots img { max-height: 260px; }
}

/* Narrow phones. */
@media (max-width: 420px) {
  .navdrop-here { max-width: 34vw; }
  .btn-lg { padding: 11px 18px; font-size: .94rem; }
  /* Two buttons side by side no longer fit; let them take a line each. */
  .card-foot .row > .btn, .card-foot .row > form { flex: 1 1 auto; }
}

/* --- Touch ------------------------------------------------------------------
   Keyed to the pointer, not the viewport: a phone in landscape is still a
   thumb, and a narrow desktop window is still a mouse. */
@media (pointer: coarse) {
  .btn { min-height: var(--tap); padding: 10px 18px; }
  .btn-sm { min-height: 38px; padding: 8px 14px; font-size: .85rem; }
  .btn-lg { min-height: 50px; }

  /* 16px is the threshold below which iOS Safari zooms the page in on focus,
     leaving the field half off-screen and needing a pinch to get back. */
  input[type=text], input[type=email], input[type=password], input[type=search],
  textarea, select {
    font-size: 16px; min-height: var(--tap); padding: 11px 12px;
  }
  textarea { min-height: 88px; }
  /* Beats `font: inherit` on the composer, which would otherwise hold it at
     15px and make iOS zoom in every time the field is tapped. */
  .console-composer textarea { font-size: 16px; }

  .perm { padding: 14px; }
  .perm input { width: 20px; height: 20px; }

  .nav-user { min-height: var(--tap); display: inline-flex; align-items: center; }
  .pagination .btn { min-width: 92px; }
  /* "show" on an activity row is four characters; give it something to hit. */
  .con-act-more summary { padding: 6px 0; }

  /* Enter sends only where there is a keyboard to press Shift+Enter on -- see
     console.js, which binds the key under the same condition. On a phone Enter
     has to stay a newline, or a multi-line message cannot be typed at all. */
  .con-key-hint { display: none; }
}

/* --- Installed as an app ----------------------------------------------------
   With the browser chrome gone the panel owns the whole screen, including the
   parts the system draws over: the status bar at the top and the home
   indicator at the bottom. Last in the file, because these have to outrank the
   small-screen padding they are correcting.

   Two spellings of the same condition -- the media query, and a class app.js
   sets from `navigator.standalone`, which is how iOS reports it instead. */

@media (display-mode: standalone) {
  .topbar { padding-top: env(safe-area-inset-top); }
  .page { padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
}
html.installed .topbar { padding-top: env(safe-area-inset-top); }
html.installed .page { padding-bottom: calc(56px + env(safe-area-inset-bottom)); }

/* --- AI dashboard -----------------------------------------------------------
   Bars rather than a chart library: this is one number per day, and a
   dependency that draws it would be larger than the page it draws on. */
.usage-days {
  display: flex; align-items: flex-end; gap: 4px;
  list-style: none; margin: 0 0 12px; padding: 0;
  height: 120px;
}
.usage-days > li {
  flex: 1; display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center; gap: 4px; height: 100%; min-width: 0;
}
.usage-day-bar {
  width: 100%; border-radius: 3px 3px 0 0;
  background: var(--accent); opacity: .85;
  /* --share is set per bar; the busiest day is full height. */
  height: var(--share, 0%);
  min-height: 2px;
}
.usage-days > li:hover .usage-day-bar { opacity: 1; }
.usage-day-label {
  font-size: .65rem; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}

/* --- Tools ------------------------------------------------------------------ */
.tool-card { display: block; padding: 20px 22px; color: inherit; }
.tool-card:hover { text-decoration: none; border-color: var(--accent); box-shadow: var(--shadow); }
.tool-card .badge { margin-top: 10px; }
.tool-name { font-weight: 600; margin-bottom: 4px; }
.tool-card:hover .tool-name { color: var(--accent); }
