/* =====================================================================
   RAG Solutions — application theme.

   WHAT THIS PRODUCT IS, because the styling follows from it: the customer
   does not come here to administer servers. They come here ONCE to point
   their own AI at their company, and afterwards they work by talking to
   it. The app is the doorway and the proof that the door is open. So:

     - THE CONNECTOR IS THE HERO. The copyable connector URL, the client
       id and the phrase the customer says to their assistant are the
       most important pixels on the screen; they get the mono face, a
       real surface of their own, and the one warm colour in the palette.
       ⚠ That phrase is never written down here, in the markup or in the
       dictionary — it is a server value (`startPhrase`), because the
       trigger word belongs to the deployment and a translated command
       triggers nothing. Style `.say-it`; never fill it.
     - PAPER AND CLAY, NOT NAVY AND STEEL. The whole palette is warm: an
       ivory canvas, a warm near-black, one clay accent. Navy/blue-grey
       is the default costume of enterprise software, and it says exactly
       what we do not want said here — institutional, impersonal, safe
       because it is large. The visitor's actual hesitation is "do I let
       an AI touch my company's data", and that is answered by a surface
       that feels considered and human, not by one that feels corporate.
       Warm neutrals also put us in the same room as the tool the customer
       arrives holding, which is the association we want doing quiet work.
       ⚠ TRADEMARK, and it is a rule not a preference (§43a): the danger
       is NOT "are we Odoo S.A." — it is "am I buying Odoo's own hosting",
       because they sell Odoo Online and Odoo.sh and we compete with both.
       So: NO Odoo purple, and no borrowed hue anywhere near the "Powered
       by Odoo Community" line. A register may be shared. A signature
       colour beside another company's name may not.
     - ONE WARM ACCENT, USED SPARINGLY. Clay (#B0563A) — a dustier, redder
       relative of the copper it replaces. Rust read as a warning colour
       at small sizes; clay reads as a hand. It marks the do-it actions
       and the connector, and is deliberately NOT in the status
       vocabulary, so a button can never be mistaken for a state.
     - STATUS IS TONAL, NEVER A TRAFFIC LIGHT. Soft tint + ink + hairline.
       Green means one thing only — the instance is live. The four status
       HUES are semantic, not brand, so they survived the re-tone intact;
       only their tints were re-mixed to sit on paper instead of on
       blue-white, and the info blue was warmed off pure cobalt.
     - HAIRLINES SEPARATE, SHADOWS DON'T LIFT. Two near-invisible warm
       layers; a third, slightly deeper, only for the connect panel.
     - CONTRAST IS A CONSTRAINT, NOT AN OUTCOME. Every token that carries
       text was re-derived to hold its old ratio — the numbers are on the
       lines below, and they are the reason these values are odd.

   Loaded by views/partials/brand-head.ejs → /login, /customer,
   /expired, /legal, /privacy, /dpa. The admin editor (index.ejs) and
   /accounts are internal surfaces and do not include it.
   Load AFTER bootstrap.min.css.
   ===================================================================== */
:root {
  /* --- Ink: a warm near-black that carries chrome, headings and the hero.
         It replaces a deep navy (#0f1b2c). Same job, opposite temperature:
         navy is read as an INSTITUTION, warm black as an OBJECT — and the
         hero panel is the largest single field of colour in the product,
         so this token alone decides which of the two the app feels like.
         Not pure black: at #201d1a the panel still has a temperature, and
         white type on it stays at 16.5:1. --- */
  --ink:           #201d1a;
  --ink-2:         #2b2723;   /* hover state of an ink surface */
  --ink-raised:    #2f2a25;   /* hero panel, one step off pure ink */
  /* Both greys carry real text (footer links, eyebrows, hints), so both clear
     4.5:1 on EVERY surface below, not just on white — 5.3:1 and 4.8:1 on the
     canvas, matching the cool greys they replace almost exactly. Anything
     lighter is decoration only. */
  --ink-muted:     #6b6257;
  --ink-faint:     #71685d;

  /* --- Surfaces. Paper, not screen: the canvas is an ivory and the cards
         a warm white, so a page reads as something laid out rather than
         something rendered.
         ⚠ THESE ARE TUNED TO LUMINANCE, NOT PICKED BY EYE. The first warm
         ramp drafted here was ~4% darker at every step than the blue-white
         one it replaced, which reads as identical on a monitor and quietly
         pushed `--live` and `--ink-faint` under 4.5:1 on the lower two
         surfaces. Each value now tracks the luminance of the cool surface
         it replaces, so ONLY the temperature moved. If you restyle these,
         re-run the contrast matrix — a warm tint costs luminance that the
         equivalent cool tint did not. --- */
  --canvas:        #f7f4ed;
  --surface:       #fffcf7;
  --surface-muted: #f3efe6;
  --surface-sunk:  #f1ece1;
  --line:          #e8e0d2;
  --line-soft:     #f2eee5;
  --line-strong:   #dad0bf;

  /* --- The one warm accent: clay. Actions, the connector, the moment
         where the AI does something for you. Redder, dustier and a shade
         lighter than the copper it replaces (#a9501e) — copper sat close
         enough to a rust/alert orange to read as a caution at 13px, which
         is the size it is used at most. White text on --accent is 4.96:1,
         so it is still safe as a solid button. --- */
  --accent:        #b0563a;
  --accent-600:    #98462d;
  --accent-700:    #7f3a25;
  --accent-tint:   #fbf0ea;
  --accent-line:   #f0d6c8;
  --accent-ink:    #8e4229;   /* accent text on white / on tint — 7.1:1 */
  --accent-on-ink: #e8a184;   /* accent text on the dark hero — 7.9:1 */

  /* --- Status vocabulary. Reserved: these four never appear on a button
         that isn't reporting state. The HUES are semantic rather than
         brand, so the re-tone left them alone; the tints were re-mixed off
         paper instead of off blue-white, and --info was pulled back from
         cobalt to a slate blue so the one cold chip in the system stops
         announcing itself as a foreign object. --- */
  --live:          #0f7a52;
  --live-tint:     #eaf2ea;
  --live-line:     #c8dcc9;
  --danger:        #b3261e;
  --danger-tint:   #fbeeeb;
  --danger-line:   #f2d1cb;
  --warn:          #8a5a00;
  --warn-tint:     #fbf3e2;
  --warn-line:     #eeddb4;
  --info:          #2f5788;
  --info-tint:     #eef2f8;
  --info-line:     #d3ddec;
  --idle:          #71685d;   /* "not started" — text, so it clears 4.5:1 too */

  /* --- Elevation. Barely there, warm-tinted, never a lift on hover. --- */
  --shadow-card: 0 1px 2px rgba(32, 29, 26, .04), 0 1px 3px rgba(32, 29, 26, .06);
  --shadow-pop:  0 4px 16px rgba(32, 29, 26, .10), 0 1px 3px rgba(32, 29, 26, .07);
  --shadow-hero: 0 12px 34px rgba(32, 29, 26, .16), 0 2px 6px rgba(32, 29, 26, .10);

  /* --- Geometry --- */
  --radius-sm: 6px;
  --radius:    10px;
  --radius-lg: 14px;
  --radius-xl: 20px;

  /* --- Type. ONE family. A display serif used to sit on top of this and
         made every page open like an advertisement; nothing replaced it —
         weight and size carry emphasis instead. Noto Sans Arabic rides in
         the same stack so the AR column (§30f) has a real face rather
         than an OS lottery. --- */
  --font-ui:   'Geist', 'Noto Sans Arabic', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Scale. Deliberately short: 15px body, and headings that stop at 1.5rem.
     Nothing on a working screen needs to be bigger than that — a large
     headline is a thing you read once, and these are screens people open
     every day. */
  --fs-xs:   .75rem;     /* 12 — meta only */
  --fs-sm:   .8125rem;   /* 13 — dense rows, hints */
  --fs-md:   .875rem;    /* 14 — buttons, secondary UI */
  --fs-base: .9375rem;   /* 15 — body */
  --fs-lg:   1.0625rem;  /* 17 — the one line that opens a page */

  /* --- Bootstrap remap: our tokens, its class names. --- */
  --bs-primary:          var(--accent);
  --bs-primary-rgb:      176, 86, 58;
  --bs-link-color:       var(--accent-ink);
  --bs-link-color-rgb:   142, 66, 41;
  --bs-link-hover-color: var(--accent-700);
  --bs-body-color:       var(--ink);
  --bs-body-bg:          var(--canvas);
  --bs-body-font-family: var(--font-ui);
  --bs-body-font-size:   var(--fs-base);
  --bs-border-color:     var(--line);
  --bs-border-radius:    var(--radius);
  --bs-font-monospace:   var(--font-mono);
}

/* =====================================================================
   Base + type
   ===================================================================== */
body {
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  letter-spacing: -.011em;   /* Inter tightens well; headings look set, not typed */
  color: var(--ink);
}

/* The one line that opens a page. Not a headline — a statement of what
   this screen is. Kept small on purpose: at 1.5rem in medium weight it
   reads as a label on a tool; at 3rem in a serif it read as a slogan. */
.u-display {
  font-family: var(--font-ui);
  font-weight: 500;
  letter-spacing: -.018em;
  line-height: 1.25;
  color: var(--ink);
}
.u-display-xl { font-size: 1.5rem; }
.u-display-lg { font-size: 1.375rem; }
.u-display-md { font-size: 1.125rem; }

/* The eyebrow: small, tracked, uppercase — the "what section is this"
   marker. Used sparingly; two of them on one screen is one too many. */
.u-eyebrow {
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.u-lead   { font-size: var(--fs-md); color: var(--ink-muted); line-height: 1.5; }
.u-muted  { color: var(--ink-muted); }
.u-faint  { color: var(--ink-faint); }
.u-mono   { font-family: var(--font-mono); font-size: .9em; letter-spacing: -.01em; }
.text-muted { color: var(--ink-muted) !important; }
small, .small { font-size: var(--fs-sm); }

a { color: var(--accent-ink); text-decoration: none; }
a:hover { color: var(--accent-700); text-decoration: underline; text-underline-offset: .18em; }
a.u-plain, a.u-plain:hover { text-decoration: none; }

code, kbd, samp, pre { font-family: var(--font-mono); }
code { color: var(--accent-ink); background: var(--accent-tint); padding: .1em .35em; border-radius: 4px; font-size: .875em; }

hr { border-color: var(--line); opacity: 1; }

::selection { background: rgba(176, 86, 58, .16); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* =====================================================================
   Buttons. Primary = clay (the one "do it"), dark = ink for the
   in-hero action, everything else quiet and tonal.
   ===================================================================== */
.btn {
  --bs-btn-border-radius: 8px;
  --bs-btn-font-size: var(--fs-md);
  --bs-btn-font-weight: 500;
  --bs-btn-padding-y: .45rem;
  --bs-btn-padding-x: .9rem;
  letter-spacing: -.005em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  transition: background-color .14s ease, border-color .14s ease, color .14s ease;
}
.btn-lg { --bs-btn-font-size: 1rem; --bs-btn-padding-y: .62rem; --bs-btn-padding-x: 1.2rem; }
.btn-sm { --bs-btn-font-size: var(--fs-sm); --bs-btn-padding-y: .3rem; --bs-btn-padding-x: .65rem; }
.btn i { font-size: 1.02em; line-height: 1; }

.btn-primary {
  --bs-btn-bg: var(--accent);
  --bs-btn-border-color: var(--accent);
  --bs-btn-hover-bg: var(--accent-600);
  --bs-btn-hover-border-color: var(--accent-600);
  --bs-btn-active-bg: var(--accent-700);
  --bs-btn-active-border-color: var(--accent-700);
  --bs-btn-disabled-bg: #dcc7bb;
  --bs-btn-disabled-border-color: #dcc7bb;
  --bs-btn-disabled-color: #fff;
}
/* The navy action: used where copper would fight the surface it sits on
   (inside the dark connect panel, copper-on-navy is the accent TEXT). */
.btn-ink {
  --bs-btn-color: #fff;
  --bs-btn-bg: var(--ink);
  --bs-btn-border-color: var(--ink);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--ink-2);
  --bs-btn-hover-border-color: var(--ink-2);
  --bs-btn-active-bg: #141210;
  --bs-btn-active-border-color: #141210;
  --bs-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--ink-faint);
  --bs-btn-disabled-border-color: var(--ink-faint);
}
.btn-outline-primary {
  --bs-btn-color: var(--accent-ink);
  --bs-btn-border-color: var(--accent-line);
  --bs-btn-bg: var(--surface);
  --bs-btn-hover-bg: var(--accent-tint);
  --bs-btn-hover-color: var(--accent-700);
  --bs-btn-hover-border-color: var(--accent);
  --bs-btn-active-bg: var(--accent-tint);
  --bs-btn-active-color: var(--accent-700);
  --bs-btn-active-border-color: var(--accent);
}
/* Secondary actions are TONAL: soft surface, hairline, ink text. */
.btn-outline-secondary {
  --bs-btn-color: var(--ink-muted);
  --bs-btn-border-color: var(--line);
  --bs-btn-bg: var(--surface);
  --bs-btn-hover-bg: var(--surface-muted);
  --bs-btn-hover-border-color: var(--line-strong);
  --bs-btn-hover-color: var(--ink);
  --bs-btn-active-bg: var(--surface-sunk);
  --bs-btn-active-border-color: var(--line-strong);
  --bs-btn-active-color: var(--ink);
  --bs-btn-disabled-color: var(--ink-faint);
  --bs-btn-disabled-border-color: var(--line-soft);
}
.btn-secondary {
  --bs-btn-color: var(--ink);
  --bs-btn-bg: var(--surface-muted);
  --bs-btn-border-color: var(--line);
  --bs-btn-hover-color: var(--ink);
  --bs-btn-hover-bg: var(--surface-sunk);
  --bs-btn-hover-border-color: var(--line-strong);
  --bs-btn-active-bg: var(--surface-sunk);
  --bs-btn-active-border-color: var(--line-strong);
}
/* Start / Run: tonal green, because it announces a STATE change to live. */
.btn-success {
  --bs-btn-color: var(--live);
  --bs-btn-bg: var(--live-tint);
  --bs-btn-border-color: var(--live-line);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--live);
  --bs-btn-hover-border-color: var(--live);
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #0b6544;
  --bs-btn-active-border-color: #0b6544;
  --bs-btn-disabled-color: var(--ink-faint);
  --bs-btn-disabled-bg: var(--surface-muted);
  --bs-btn-disabled-border-color: var(--line-soft);
}
.btn-warning {
  --bs-btn-color: var(--warn);
  --bs-btn-bg: var(--warn-tint);
  --bs-btn-border-color: var(--warn-line);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--warn);
  --bs-btn-hover-border-color: var(--warn);
}
.btn-danger {
  --bs-btn-bg: var(--danger);
  --bs-btn-border-color: var(--danger);
  --bs-btn-hover-bg: #97201a;
  --bs-btn-hover-border-color: #97201a;
}
.btn-outline-danger {
  --bs-btn-color: var(--ink-muted);
  --bs-btn-border-color: var(--line);
  --bs-btn-bg: var(--surface);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--danger);
  --bs-btn-hover-border-color: var(--danger);
  --bs-btn-active-bg: #97201a;
  --bs-btn-active-border-color: #97201a;
}
.btn-link { --bs-btn-color: var(--ink-muted); --bs-btn-hover-color: var(--ink); text-decoration: none; }

/* Icon-only control: square, quiet, never louder than the row it sits in. */
.btn-icon {
  --bs-btn-padding-x: .5rem;
  --bs-btn-padding-y: .38rem;
  line-height: 1;
}

/* =====================================================================
   Forms — hairline border, generous target, quiet copper focus ring.
   ===================================================================== */
.form-control, .form-select {
  border-color: var(--line);
  border-radius: 8px;
  color: var(--ink);
  font-size: var(--fs-base);
  padding: .5rem .75rem;
  background-color: var(--surface);
}
.form-control::placeholder { color: var(--ink-faint); }
.form-control:focus, .form-select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(176, 86, 58, .13);
}
.form-control-lg { font-size: var(--fs-lg); padding: .62rem .85rem; }
/* Labels are sentence case now, not tracked caps: the caps idiom belongs
   to an operator console and shouts at a first-time customer. */
.form-label {
  color: var(--ink);
  font-size: var(--fs-md);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  margin-bottom: .3rem;
}
.form-text { color: var(--ink-muted); font-size: var(--fs-sm); line-height: 1.45; }
.input-group-text {
  background: var(--surface-muted);
  border-color: var(--line);
  color: var(--ink-muted);
  border-radius: 8px;
}
.form-check-input:checked { background-color: var(--accent); border-color: var(--accent); }
.form-check-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(176, 86, 58, .13); }

/* =====================================================================
   Surfaces
   ===================================================================== */
.card {
  border-color: var(--line-soft);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.card.shadow-sm { box-shadow: var(--shadow-card) !important; }
.card-body { padding: 1.15rem 1.25rem; }

/* A plain panel where a Bootstrap card would be too much chrome. */
.panel {
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.panel-sunk { background: var(--surface-muted); border-color: var(--line-soft); box-shadow: none; }

/* =====================================================================
   Status badges — tonal, never a solid traffic light. Bootstrap's
   .text-bg-* set colour+background with !important, so these need both
   !important and the extra .badge class to win. The JS keeps emitting
   the same class names; only the look changes.
   ===================================================================== */
.badge {
  --bs-badge-font-weight: 500;
  --bs-badge-padding-x: .6em;
  --bs-badge-padding-y: .42em;
  font-size: var(--fs-xs);
  border-radius: 999px;
  border: 1px solid transparent;
  letter-spacing: .005em;
  display: inline-flex;
  align-items: center;
  gap: .3rem;
}
.badge.text-bg-success   { color: var(--live) !important;      background-color: var(--live-tint) !important;    border-color: var(--live-line); }
.badge.text-bg-danger    { color: var(--danger) !important;    background-color: var(--danger-tint) !important;  border-color: var(--danger-line); }
.badge.text-bg-warning   { color: var(--warn) !important;      background-color: var(--warn-tint) !important;    border-color: var(--warn-line); }
.badge.text-bg-primary   { color: var(--info) !important;      background-color: var(--info-tint) !important;    border-color: var(--info-line); }
.badge.text-bg-info      { color: var(--ink-muted) !important; background-color: var(--surface-muted) !important; border-color: var(--line); }
.badge.text-bg-secondary { color: var(--idle) !important;      background-color: var(--surface-muted) !important; border-color: var(--line-soft); }
.badge.text-bg-light     { color: var(--ink-muted) !important; background-color: var(--surface-muted) !important; border-color: var(--line-soft); }

/* The live dot: the smallest possible "it is running" signal, used on the
   instance card beside the name. Pulses ONLY while provisioning. */
.dot { width: .5rem; height: .5rem; border-radius: 50%; display: inline-block; flex: 0 0 auto; background: var(--idle); }
.dot-live { background: var(--live); box-shadow: 0 0 0 3px var(--live-tint); }
.dot-busy { background: var(--info); box-shadow: 0 0 0 3px var(--info-tint); animation: pulse 1.6s ease-in-out infinite; }
.dot-failed { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-tint); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Alerts follow the same tonal rule. */
.alert { border-radius: var(--radius); border-width: 1px; font-size: var(--fs-md); padding: .7rem .9rem; }
.alert-danger  { color: var(--danger); background: var(--danger-tint); border-color: var(--danger-line); }
.alert-warning { color: var(--warn);   background: var(--warn-tint);   border-color: var(--warn-line); }
.alert-success { color: var(--live);   background: var(--live-tint);   border-color: var(--live-line); }
.alert-info    { color: var(--info);   background: var(--info-tint);   border-color: var(--info-line); }

/* =====================================================================
   Top bar — flat ink, one hairline. The brand chip stays white so our
   mark keeps its own field on the dark bar (§43: it is OUR mark; the
   Odoo reference lives in words, never in an image).
   ===================================================================== */
.navbar.brand-bar {
  background: var(--ink) !important;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, .07);
  min-height: 58px;
  padding-top: .5rem;
  padding-bottom: .5rem;
}
.navbar.brand-bar .navbar-brand {
  display: inline-flex; align-items: center; gap: .6rem;
  color: #fff; font-size: var(--fs-md); font-weight: 600; letter-spacing: -.005em;
}
.navbar.brand-bar .navbar-brand img { height: 22px; display: block; }
.navbar.brand-bar .navbar-text { color: rgba(255, 255, 255, .68); font-size: var(--fs-sm); }
.navbar.brand-bar-app { background: var(--ink) !important; }
.brand-logo-chip {
  background: var(--surface); border-radius: 7px; padding: .28rem .5rem;
  display: inline-flex; align-items: center;
}
.navbar.brand-bar .brand-logo-chip img { height: 18px; display: block; }
/* Divider between the mark and the page name in the bar. */
.brand-sep { width: 1px; height: 20px; background: rgba(255, 255, 255, .18); display: inline-block; }
.navbar.brand-bar .btn-outline-light {
  --bs-btn-color: rgba(255, 255, 255, .74);
  --bs-btn-border-color: rgba(255, 255, 255, .2);
  --bs-btn-hover-bg: rgba(255, 255, 255, .1);
  --bs-btn-hover-border-color: rgba(255, 255, 255, .34);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: rgba(255, 255, 255, .18);
  font-size: var(--fs-sm);
  letter-spacing: 0;
}
/* Language switcher: quiet pills, the active one filled. */
.lang-switch .btn { --bs-btn-padding-y: .15rem; --bs-btn-padding-x: .5rem; font-size: var(--fs-xs); font-weight: 500; }
.lang-switch .btn.active { background: rgba(255, 255, 255, .16); color: #fff; border-color: rgba(255, 255, 255, .34); }

/* =====================================================================
   Modals
   ===================================================================== */
.modal-content { border: 1px solid var(--line-soft); border-radius: var(--radius-xl); box-shadow: var(--shadow-pop); }
.modal-header { border-bottom-color: var(--line-soft); padding: 1.15rem 1.4rem .95rem; }
.modal-header .modal-title { font-size: 1.05rem; font-weight: 600; color: var(--ink); }
.modal-body { padding: 1.35rem 1.4rem; }
.modal-footer { border-top-color: var(--line-soft); background: rgba(240, 235, 225, .55); padding: .9rem 1.4rem; border-radius: 0 0 var(--radius-xl) var(--radius-xl); }
.modal-backdrop.show { opacity: .45; background: var(--ink); }

/* =====================================================================
   THE CONNECT STEPS.

   ⚠ THIS USED TO BE A DARK HERO PANEL, and the reason it no longer is
   generalises. It was drawn when connecting WAS the page — one slab of
   ink carrying the whole product, so the eye had to land on it first.
   Inside the rail it is one section of three, reached by pressing its
   own name, so nothing is competing for that first look; a dark field
   in the middle of a settings screen stops reading as emphasis and
   starts reading as an advertisement. Plain, on the same ground as the
   rest, is now the honest weight. `.copyfield-dark`, `.say-it-light`,
   `.connect-lead` and the panel's own colour overrides went with it.

   The steps themselves are unchanged in structure: the whole onboarding
   visible at once, because a customer who cannot see the end of the
   process does not start it. Three is the maximum this may ever grow to.
   ===================================================================== */
.connect-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.35rem; }
.connect-step { display: grid; grid-template-columns: 1.6rem 1fr; gap: .8rem; align-items: start; }
.connect-step-n {
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 600;
  background: var(--accent-tint); color: var(--accent-ink);
  border: 1px solid var(--accent-line);
}
.connect-step-title { font-weight: 600; font-size: var(--fs-md); color: var(--ink); margin-bottom: .3rem; }
.connect-step-body { color: var(--ink-muted); font-size: var(--fs-sm); }
/* The closing note under the steps: what the assistant can do once the instance
   is up. Set apart by a tint and a hairline rather than by weight — it is not a
   step, and giving it a step's emphasis would read as a fifth thing to do at
   the end of a list of four. */
.connect-note {
  margin: 1.6rem 0 0;
  padding: .85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-muted);
  color: var(--ink-muted);
  font-size: var(--fs-sm);
  line-height: 1.55;
}

/* The worked example in step 1: a VALUE to copy into Claude, so it is drawn
   like one — a quiet chip, not a form control and not a heading. It replaced a
   real text input (`.connect-field-label` went with it): the customer reads
   this and types their own company name. */
.connect-example {
  display: inline-block;
  padding: .25rem .6rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--ink-muted);
  font-size: var(--fs-sm);
}
.connect-step-body ol, .connect-step-body ul { padding-inline-start: 1.1rem; margin-bottom: 0; }
.connect-step-body li { margin-bottom: .2rem; }

/* The copy field: a mono value that exists to be grabbed. */
.copyfield {
  display: flex; align-items: stretch; gap: 0;
  border-radius: 9px; overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface);
}
.copyfield input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: transparent; color: var(--ink);
  font-family: var(--font-mono); font-size: var(--fs-sm);
  padding: .5rem .7rem;
}
.copyfield input:focus { outline: none; }
.copyfield button {
  flex: 0 0 auto; border: 0; border-inline-start: 1px solid var(--line);
  background: var(--surface-muted); color: var(--ink-muted);
  font-size: var(--fs-sm); font-weight: 500;
  padding: .5rem .8rem; display: inline-flex; align-items: center; gap: .35rem;
  transition: background-color .14s ease, color .14s ease;
}
.copyfield button:hover { background: var(--surface-sunk); color: var(--ink); }
.copyfield button.is-copied { background: var(--live-tint); color: var(--live); }

/* ⚠ CURRENTLY UNUSED — nothing on any page carries `.say-it` since the connect
   page's step 4 stopped teaching one copyable phrase (`startPhrase`) and started
   teaching the habit of opening a prompt with the connector's name. Kept because
   the rule below is the expensive part to rediscover, not the three declarations.

   It styled the one phrase the whole product turned on: treated as an object, not
   as text — mono, bordered, clay — so it was unmistakable in a video.

   ⚠ IF ANYTHING RENDERS A TRIGGER PHRASE AGAIN, ITS TEXT BELONGS IN NEITHER THE
   CSS, THE MARKUP NOR THE DICTIONARY. It comes from the server (`startPhrase`),
   because the connector's trigger word is a deployment's to choose and a
   translated command triggers nothing. `.say-it-text` is the span the script
   writes into. */
.say-it {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-mono); font-size: var(--fs-md); font-weight: 500;
  color: var(--accent-ink); background: var(--accent-tint);
  border: 1px solid var(--accent-line);
  border-radius: 999px; padding: .35rem .85rem;
  cursor: pointer;
}
.say-it:hover { border-color: var(--accent); }
.say-it.is-copied { color: var(--live); background: var(--live-tint); border-color: var(--live-line); }
/* Sized to sit inside a line of text (the empty state) rather than to be
   the point of a step. */
.say-it-sm { font-size: var(--fs-sm); padding: .25rem .7rem; }

/* =====================================================================
   THE WORKSPACE — a left rail and one content pane, in the shape of
   Odoo's own configuration screen.

   WHY A RAIL. The customer surface has three jobs with three different
   rhythms: the platform changes minute to minute, the connector is set
   up once, the account is consulted a few times a year. Stacked in one
   column the two a RETURNING customer wants sat below a setup they
   finished weeks ago. Named sections cost nothing and each one gets the
   whole height.

   ⚠ ONE SET OF MARKUP FOR BOTH LAYOUTS. Below 768px the same rail
   becomes a horizontal scrolling tab row — no duplicated buttons, and
   therefore no second copy of three labels to translate and keep in
   step. Everything here is logical-direction (inline-start/end), so RTL
   mirrors without a single extra rule.
   ===================================================================== */
.workspace {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 1rem;
}
@media (min-width: 768px) {
  .workspace {
    grid-template-columns: 13.5rem minmax(0, 1fr);
    gap: 2.25rem;
    padding: 0 1.5rem;
  }
}

.ws-rail { display: flex; }
@media (min-width: 768px) {
  .ws-rail {
    flex-direction: column;
    gap: .15rem;
    /* Sticky so the sections stay reachable while a long instance list
       scrolls — the rail is navigation, and navigation you have to
       scroll back up to find is a menu you stop using. */
    position: sticky;
    top: 1.5rem;
    align-self: start;
    padding-top: 2rem;
  }
}
/* Mobile: a tab row under the brand bar. overflow-x rather than wrapping,
   so three items never become two lines and shove the content down. */
@media (max-width: 767.98px) {
  .ws-rail {
    gap: .25rem;
    overflow-x: auto;
    scrollbar-width: none;
    margin: 0 -1rem;
    padding: .6rem 1rem;
    border-bottom: 1px solid var(--line-soft);
  }
  .ws-rail::-webkit-scrollbar { display: none; }
  .ws-rail-item { flex: 0 0 auto; }
}

.ws-rail-item {
  display: flex; align-items: center; gap: .55rem;
  width: 100%;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--ink-muted);
  font-size: var(--fs-md); font-weight: 500;
  text-align: start;
  padding: .5rem .7rem;
  white-space: nowrap;
  transition: background-color .14s ease, color .14s ease, border-color .14s ease;
}
.ws-rail-item i { font-size: 1.05em; opacity: .8; }
.ws-rail-item:hover { background: var(--surface-muted); color: var(--ink); }
.ws-rail-item.is-active {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-ink);
  font-weight: 600;
}
.ws-rail-item.is-active i { opacity: 1; }

/* ⚠ THE FOOTER IS IN ORDINARY FLOW. Two mechanisms for pushing it to the foot
   of the VIEWPORT were tried and both are gone: `min-height: calc(100vh -
   <navbar>)` on the content (a guess at the bar's height — wrong by a few
   pixels, so the page carried a scrollbar that scrolled nothing), then
   `body.workspace-doc` as a flex column with the workspace taking the slack
   (correct, but on the short sections it left ~750px of empty page above a lone
   hairline, which reads as a fault rather than as layout). A footer belongs
   where the page ends. `position: fixed` is not the answer either — it would
   cover content and eat height on a phone. */
.ws-content { min-width: 0; padding-bottom: 1rem; }

/* The page-level legal footer's box. ⚠ It MUST match `.workspace` above,
   because `.app-footer` has no width or horizontal padding of its own: it
   inherits them from whatever container it sits in, and on this page it sits in
   none. Without this the footer spanned the whole window and its text touched
   both screen edges on a phone. Keep the two in lockstep. */
.workspace-foot { max-width: 1120px; margin: 0 auto; padding: 0 1rem; }
@media (min-width: 768px) { .workspace-foot { padding: 0 1.5rem; } }
.ws-pane { padding-top: 2rem; }
/* The heading row: the section's name, and the one action that belongs to
   it. `space-between` with a single child still puts the title where it
   should be, so a pane with no action needs no variant class. */
.ws-pane-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-bottom: 1.5rem;
}
.ws-pane-title {
  font-size: 1.25rem; font-weight: 600; letter-spacing: -.015em;
  margin: 0; color: var(--ink);
}

/* A block inside a pane: a small caps-ish heading and its content, divided
   by a hairline rather than boxed. Boxes inside a pane inside a rail is one
   frame too many. */
.ws-block { padding-bottom: 1.6rem; margin-bottom: 1.6rem; border-bottom: 1px solid var(--line-soft); }
.ws-block-last { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
.ws-block-title {
  font-size: var(--fs-sm); font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 .9rem;
}

/* Label/value pairs. Two columns where there is room, stacked where there
   is not — a definition list because that is what it is. */
.ws-facts { margin: 0; display: grid; gap: .55rem; }
.ws-fact { display: grid; grid-template-columns: 1fr; gap: .1rem; }
@media (min-width: 576px) {
  .ws-fact { grid-template-columns: 11rem minmax(0, 1fr); gap: .9rem; align-items: baseline; }
}
.ws-fact dt { font-size: var(--fs-sm); font-weight: 400; color: var(--ink-faint); }
.ws-fact dd { margin: 0; font-size: var(--fs-base); color: var(--ink); }

.ws-langs { display: flex; flex-wrap: wrap; gap: .4rem; }
/* i18n.js adds `.active` to whichever data-set-lang matches the current
   language; give it the same weight the rail's active item has, so "which
   language am I in" is answered the same way everywhere on the page. */
.ws-langs .btn.active {
  background: var(--accent-tint); border-color: var(--accent); color: var(--accent-ink); font-weight: 600;
}

/* `.ws-docs` (the Profile pane's list of the four legal documents, each with a
   line of prose) is DELETED with the markup it dressed: the legal footer moved
   into this pane, so those links now render once, at its foot. */

/* =====================================================================
   Instance card — "your company's data lives here", not a server row.
   ===================================================================== */
.app-card {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  transition: border-color .15s ease;
}
.app-card:hover { border-color: var(--line); }
.app-card-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.app-card-name { font-size: 1.0625rem; font-weight: 600; letter-spacing: -.01em; }
.app-card-meta { font-size: var(--fs-sm); color: var(--ink-muted); }
.app-card-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

/* ---------------------------------------------------------------------
   The build, on the instance's own line.

   This is where "your platform is being created" is said now — the New-instance
   wizard used to say it, behind a modal, for the minutes it takes. It is two
   rows: a bar, and the step under it. Nothing here is decoration; the bar is a
   COUNT of the run's finished nodes (customer.js feedProgress) and the line
   under it is the node now running, in the customer's own words.

   ⚠ IT MUST NOT RESIZE THE CARD WHEN IT APPEARS. A card that grows and shrinks
   as instances start and finish makes a list of them jump under the reader. The
   block is a fixed height whether it carries one line or none, and it only ever
   sits between the meta line and the actions.
   --------------------------------------------------------------------- */
.app-card-progress { margin-top: .8rem; }
/* The plain-language line: what is happening, for someone who does not care
   which step it is on. It carries the weight the step line deliberately does
   not — same size as the body, ink rather than muted. */
.app-card-prov-title {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: .4rem;
}
.app-card-progress .progress {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-sunk);
  overflow: hidden;
}
.app-card-progress .progress-bar {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--accent);
  transition: width .4s ease;
}
/* No count to show (the stream cannot say, or the run is finishing): a bar that
   travels rather than one that claims a position. */
.app-card-progress .progress.is-indeterminate .progress-bar {
  width: 100% !important;
  background: linear-gradient(90deg,
    var(--accent-line) 0%, var(--accent) 45%, var(--accent) 55%, var(--accent-line) 100%);
  background-size: 250% 100%;
  animation: prog-sweep 1.4s ease-in-out infinite;
}
@keyframes prog-sweep {
  from { background-position: 130% 0; }
  to   { background-position: -30% 0; }
}
.app-card-step {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  margin-top: .4rem;
  font-size: var(--fs-sm);
  color: var(--ink-muted);
  min-height: 1.15rem;   /* holds its line so the card does not jump between steps */
}
.app-card-step .step-count { color: var(--ink-faint); font-size: var(--fs-xs); }

/* The sweep is the only thing here that moves; a still bar still says "this is
   running" because the label under it does. (The global reduced-motion rule
   collapses TRANSITIONS but an animation has to be named.) */
@media (prefers-reduced-motion: reduce) {
  .app-card-progress .progress.is-indeterminate .progress-bar {
    animation: none;
    background: var(--accent-line);
  }
}

/* The PROMPT LIBRARY's grid and chips lived here. The section was removed from
   /customer (owner's call, 2026-08-12): a list of things the customer's own AI
   can be asked reads as marketing on a surface they have already bought, and
   the only line that was an instruction rather than an example — `orcha start`
   — moved to the connected strip, where somebody who has just finished
   connecting is actually looking. `.say-it` / `.say-it-sm` carry it. */

/* Section heading with a hairline rule running to the end of the row. */
.section-head { display: flex; align-items: center; gap: .8rem; margin-bottom: .85rem; }
.section-head::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--line-soft); }

/* =====================================================================
   Wizard (browser path — secondary to the connector, but still complete)
   ===================================================================== */
.wiz-steps { list-style: none; padding: 0; margin: 0; }
.wiz-step {
  display: flex; align-items: center; gap: .45rem;
  font-size: var(--fs-sm); color: var(--ink-faint);
  padding-bottom: .5rem; border-bottom: 2px solid var(--line-soft);
  transition: color .15s, border-color .15s;
}
.wiz-step .wiz-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.35rem; height: 1.35rem; border-radius: 50%;
  font-weight: 600; font-size: var(--fs-xs);
  background: var(--surface-muted); color: var(--ink-faint);
  border: 1px solid var(--line); flex: 0 0 auto;
}
.wiz-step.active { color: var(--ink); border-bottom-color: var(--accent); font-weight: 600; }
.wiz-step.active .wiz-dot { background: var(--accent); border-color: var(--accent); color: #fff; }
.wiz-step.done { color: var(--ink-muted); border-bottom-color: var(--line-strong); }
.wiz-step.done .wiz-dot { background: var(--surface); border-color: var(--live-line); color: var(--live); }
.wiz-step-label { display: none; }
@media (min-width: 576px) { .wiz-step-label { display: inline; } }

/* =====================================================================
   Auth screen (/login) — ONE CENTRED COLUMN.

   It was a split: a navy story panel beside a white form. The panel had
   already been emptied to a single sentence, and half a screen of gradient
   holding one line is a stage with nobody on it — it also never rendered
   below 992px, which is where most first visits happen, so the "real"
   design was the fallback. Everything is on one axis now: mark, what this
   is, the door, the other door, the fine print.

   360px is the column, matching the form width the split layout already
   used — the change is the page around it, not the fields.

   ⚠ THE OLD .auth-brand / .auth-shell / .auth-form-wrap RULES ARE GONE, and
   so are the desktop/mobile logo pair and the corner-swap they needed. If a
   dark panel is ever wanted again it is a new surface, not a revert: the
   reasoning against it is in views/login.ejs.
   ===================================================================== */
/* ---------------------------------------------------------------------
   PLAIN SURFACE — white, and in the system UI face.

   Worn by the two pages the CUSTOMER sees: /login and /customer (owner's
   request, 2026-08-12 — the second followed the first the same day, so the
   ground does not change under somebody at the moment they sign in).
   Everything else — the admin editor, /accounts, the legal documents —
   keeps the warm paper the :root block argues for: an ivory canvas and a
   warm-white surface, tuned to luminance so the greys hold their contrast.

   ⚠ IT OVERRIDES TOKENS, NOT PROPERTIES. That is what makes it a class and
   not a rewrite: every descendant — buttons, inputs, panels, hairlines —
   follows from --surface/--line without one component being special-cased,
   and removing the class returns the page to the theme exactly.

   THE FACE is the stack Claude's own apps fall back to. Styrene and Tiempos
   are licensed typefaces that cannot be redistributed, so what is honest to
   ship is the same chain minus the proprietary names: the platform UI face
   (SF on macOS, Segoe UI Variable on Windows). Noto Sans Arabic stays in
   the chain or the Arabic column loses its face.
   --------------------------------------------------------------------- */
.plain-surface {
  --font-ui: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
             'Segoe UI Variable Text', 'Segoe UI', 'Noto Sans Arabic', Arial, sans-serif;
  /* Pure white, and neutral lines to match — a warm beige hairline on a cold
     white ground reads as a rendering fault rather than a choice. Text keeps
     the brand's warm ink: --ink-faint is 4.6:1 on #fff, so the fine print
     stays legible. */
  --surface: #ffffff;
  --surface-muted: #f6f6f5;
  --canvas: #ffffff;
  --line: #e4e2df;
  --line-soft: #efeeec;
  --line-strong: #d3d1cd;
  background: #fff;
  font-family: var(--font-ui);
}

/* Two rows: the column takes the space and centres in it, the attribution
   sits on the page's bottom edge. A grid, not flex+justify-content, because
   the attribution has to reach the BOTTOM while the column stays centred in
   what is left — and when the content outgrows the viewport both simply
   scroll. */
.auth-page {
  min-height: 100vh; background: #fff;
  display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr;
  padding: 4.5rem 1.25rem 1.5rem;
}
.auth-col { width: 100%; max-width: 360px; align-self: center; justify-self: center; }

.auth-mark { text-align: center; margin-bottom: 1.75rem; }
.auth-mark img { height: 30px; display: inline-block; }

/* The product sentence, promoted off the retired panel. Centred with the rest
   of the column and one step down from the panel's 1.5rem: it introduces the
   page, it is not a banner. max-width keeps it to two lines at 360px, which is
   where a heading stops being read and starts being skimmed. */
.auth-h1 {
  font-family: var(--font-ui); font-weight: 500;
  font-size: 1.375rem; line-height: 1.3; letter-spacing: -.018em;
  color: var(--ink); text-align: center; margin: 0 auto .4rem;
}
.auth-sub {
  text-align: center; color: var(--ink-muted); font-size: var(--fs-md);
  margin: 0 0 1.75rem;
}

/* The labelled separator between the two ways in. The rules are drawn by the
   pseudo-elements so the label sits IN the line rather than on top of it, and
   the whole thing is one step quieter than the form labels around it — it
   names a section, it is not an instruction. */
.auth-sep {
  display: flex; align-items: center; gap: .75rem;
  margin: 1.7rem 0 1.35rem;
  color: var(--ink-faint); font-size: var(--fs-sm);
}
.auth-sep::before, .auth-sep::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--line-soft);
}
/* The fine print — notice, links, entity line, attribution — as ONE block with
   a single rule above it. Four separately-spaced footnotes read as leftovers;
   grouped, they read as the page's foot. */
.auth-fine { margin-top: 2rem; padding-top: 1.4rem; border-top: 1px solid var(--line-soft); }
/* One quiet row of secondary links instead of three stacked, centred blocks. */
.auth-links {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem;
  margin-top: 1.1rem; font-size: var(--fs-xs);
}
.auth-links a { color: var(--ink-faint); }
.auth-links a:hover { color: var(--ink); }
.auth-links span { color: var(--line-strong); }

/* The consent-by-action notice under the sign-in actions. One size below the
   trial reassurance and above the link row: it is a disclosure, so it must be
   legible rather than loud — --ink-faint clears 4.5:1 on every surface, which
   is the line between "quiet" and "buried". */
.auth-legal-note {
  margin: 0; text-align: center;
  font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-faint);
}
.auth-legal-note a { color: var(--ink-muted); text-decoration: underline; text-underline-offset: .16em; }
.auth-legal-note a:hover { color: var(--ink); }

/* The brand/entity line. It used to render TWICE — once on the dark panel and
   once under the form — with a media query hiding whichever was redundant.
   One column, one place, no query: this is the only copy of it now. */
.auth-entity-line {
  margin: .8rem 0 0; text-align: center;
  font-size: var(--fs-xs); color: var(--ink-faint);
}

/* The trial reassurance under the Google button. Small and quiet on purpose:
   it answers a last-moment objection, it does not make an offer — an offer
   set at button weight would turn the page back into an advertisement. */
.auth-cta-note {
  margin: .6rem 0 0; text-align: center;
  font-size: var(--fs-sm); color: var(--ink-faint); line-height: 1.45;
}

/* .auth-alt — a bare hairline above the password form — is RETIRED: .auth-sep
   draws that line and labels it now.
   HISTORY, so nobody rebuilds what came before it: the form was once a
   <details> whose summary read "Sign in with a password", on the argument that
   a disclosure costs a first-time visitor one line instead of a whole form. In
   practice it cost the people who DO use the form a click and a hunt, and made
   a sign-in screen ask "which of these am I?" — a question a login page should
   never ask. The separator keeps the one thing that summary was good for, its
   words, and hides nothing.

   `.btn-quiet` lived here for one afternoon, while Google was first and the
   password submit had to recede. The order reversed the same day (owner's
   call — see views/login.ejs), the submit became the primary action in BOTH
   configurations, and the class had no user left. */

/* Google's brand guidance: neutral surface, no recolouring of the mark. Also
   the right weight now that it is the SECONDARY action, under the form.
   `.btn-google-primary` — the heavier variant it wore while it was first —
   went with that change; if the button is ever promoted again it wants a
   deliberate weight, not a resurrected one. */
.btn-google {
  --bs-btn-color: var(--ink); --bs-btn-border-color: var(--line);
  --bs-btn-bg: var(--surface);
  --bs-btn-hover-bg: var(--surface-muted);
  --bs-btn-hover-color: var(--ink); --bs-btn-hover-border-color: var(--line-strong);
}
.btn-google .bi-google { color: #4285f4; font-size: 1rem; }

/* TRADEMARK POSTURE (§43) — the technology is named in WORDS, in our own
   type. Never an image, never a redrawn wordmark, and never a colour
   borrowed from Odoo. Naming the COMMUNITY edition is what keeps the line
   descriptive rather than a claim to be their service.

   BOTTOM-RIGHT, IN FLOW — the grid's second row, never position:fixed. Fixed,
   it hung over the fine print on a short screen and followed the reader down
   the page for no reason; in flow it sits at the document's own bottom edge and
   is reached by scrolling. inset-inline via `justify-self: end` so RTL mirrors
   it to the left without a second rule. */
/* ⚠ /login DOES NOT USE views/partials/legal-footer.ejs — it builds its own
   fine print (§49a), so `.app-footer-tech` never reaches this page and the
   attribution needs lifting here too, or the line stands out everywhere except
   the first screen a customer sees. Same direction as the footer version: off
   the faintest grey, more weight. More weight rather than full ink because
   uppercase text at 12px already reads darker than its colour suggests, and
   because position already separates it here — it is the page's own corner
   element, not a member of a row of legal links. */
.auth-powered-by {
  justify-self: end; margin-top: 2rem;
  color: var(--ink-muted); font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
}
.auth-lang-corner { position: fixed; top: 1rem; inset-inline-end: 1.25rem; z-index: 10; }
.auth-lang-corner .btn {
  --bs-btn-color: var(--ink-muted); --bs-btn-border-color: var(--line);
  --bs-btn-bg: var(--surface);
  font-size: var(--fs-xs); font-weight: 500;
}
.auth-lang-corner .btn.active { background: var(--ink); border-color: var(--ink); color: #fff; }

/* ---------------------------------------------------------------------
   THE LANGUAGE MENU (/login) — one trigger naming the current language,
   opening a list of native names.

   It replaced a four-chip segmented control: showing every option at once
   is right for two and wrong for four, and a language abbreviated to two
   latin letters is least legible to the visitor who needs it most. Built on
   <details>/<summary> so it needs no JS framework — this page loads none,
   and one menu is not a reason to start.

   Bottom-INLINE-START, opposite the attribution: two quiet utilities on the
   page's bottom edge, one per corner, and `inset-inline` mirrors both under
   RTL. Fixed, unlike the attribution, because it is a control — a visitor
   who cannot read the page must not have to scroll to find it.
   --------------------------------------------------------------------- */
.lang-corner { position: fixed; bottom: 1rem; inset-inline-start: 1.25rem; z-index: 10; }
.lang-menu { position: relative; }
.lang-trigger {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .32rem .6rem; border-radius: 8px; cursor: pointer;
  color: var(--ink-muted); font-size: var(--fs-sm); line-height: 1.2;
  background: var(--surface); border: 1px solid var(--line);
  list-style: none; /* Safari/Chrome default triangle */
  user-select: none;
}
.lang-trigger::-webkit-details-marker { display: none; }
.lang-trigger:hover { color: var(--ink); border-color: var(--line-strong); }
.lang-menu[open] .lang-trigger { color: var(--ink); border-color: var(--line-strong); }
.lang-trigger .bi-globe2 { font-size: .95em; }
.lang-caret { font-size: .7em; opacity: .7; }
.lang-menu[open] .lang-caret { transform: rotate(180deg); }

/* ⚠ A closed <details> hides its non-summary children with a UA rule that an
   AUTHOR `display` beats — so `display:flex` below left the list permanently on
   screen, floating over the fine print. Anything that sets `display` inside a
   <details> needs this line with it. */
.lang-menu:not([open]) .lang-list { display: none; }

/* Opens UPWARD: the trigger sits on the bottom edge, so a menu below it would
   leave the viewport. */
.lang-list {
  position: absolute; bottom: calc(100% + .35rem); inset-inline-start: 0;
  min-width: 11rem; padding: .3rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .09);
  display: flex; flex-direction: column;
}
.lang-item {
  appearance: none; background: none; border: 0; border-radius: 6px;
  padding: .4rem .6rem; text-align: start; cursor: pointer;
  color: var(--ink-muted); font-size: var(--fs-sm); font-family: inherit;
}
.lang-item:hover { background: var(--surface-muted); color: var(--ink); }
/* i18n.js marks the live language; a tick would need an icon per row, and the
   trigger already answers "which am I in" — this only has to answer "which one
   is this" while the list is open. */
.lang-item.active { color: var(--ink); font-weight: 500; background: var(--surface-muted); }

/* The corner-swap block that used to live here (.auth-lang-corner--on-brand,
   .auth-logo-corner, .auth-brand--swapped) went with the dark panel: it existed
   to give the mark and the language chips one corner each across two columns.
   With a single centred column the mark sits above the fields on its own axis
   and the chips keep the one corner there is — no variant, no media query. */

/* =====================================================================
   The global legal footer (views/partials/legal-footer.ejs)

   Quiet by construction. LCEN requires these disclosures to be PRESENT and
   reachable, not prominent — and a footer that competes with the working
   surface above it gets ignored by exactly the people it protects. Hairline,
   faint ink, one size down.
   ===================================================================== */
.app-footer {
  margin-top: 2.5rem; padding-top: 1.1rem;
  border-top: 1px solid var(--line-soft);
  display: flex; flex-wrap: wrap; gap: .55rem 1.1rem;
  justify-content: space-between; align-items: baseline;
  font-size: var(--fs-sm); color: var(--ink-faint);
}
.app-footer-entity { color: var(--ink-faint); }

/* THE LANGUAGE MENU IN THE FOOTER (`showLanguage` on the partial — the signed-in
   surface only).

   ⚠ IT TAKES A ROW OF ITS OWN, ON PURPOSE. `flex-basis: 100%` rather than letting it
   ride the main row: the three existing children (copyright, four links, the Odoo
   attribution) already fill ~1030px of a 1072px footer at desktop width, so a 94px
   trigger cannot fit beside them at ANY width this app is read at. Left to wrap on
   its own it landed alone against the far edge, which reads as something that fell
   off the line above. Declaring the row makes the same outcome deliberate and — more
   to the point — identical at every width, instead of depending on how long the
   translated copyright happens to be. It also leaves `space-between` distributing
   the original three exactly as it did before this was added.

   ⚠ START-ALIGNED, so it sits under the copyright's own edge rather than ragged
   right; `margin-inline-end: auto` rather than a text-align, because the child is a
   flex item. Logical property, so RTL mirrors it for free.

   ⚠ z-index so the open list is painted OVER the fine print rather than under it:
   `.lang-list` is absolutely positioned and the footer creates no stacking context of
   its own. */
.app-footer-lang { flex-basis: 100%; margin-inline-end: auto; z-index: 1; }
/* One size down, like everything else in this footer — the corner version sets the
   ambient page size; here it sits inside a smaller row. */
.app-footer-lang .lang-trigger { font-size: inherit; padding: .22rem .5rem; }
.app-footer-links { display: flex; flex-wrap: wrap; gap: .55rem 1.1rem; align-items: baseline; }
.app-footer a,
.app-footer .btn-link { color: var(--ink-faint); text-decoration: none; font-size: inherit; }
.app-footer a:hover,
.app-footer .btn-link:hover { color: var(--ink); text-decoration: underline; text-underline-offset: .18em; }
/* ⚠ THE ONE LINE IN THIS FOOTER THAT IS NOT A LEGAL DOCUMENT, and it now looks
   it. It used to sit inside the link row in the same faint grey as the four
   agreements, so it read as a fifth link that would not respond. Full ink and
   medium weight against the faint grey around it is the whole differentiation:
   NO border, chip, tint or box — see the note in views/partials/legal-footer.ejs
   for why anything shaped like a mark is the wrong move here (§43b). */
.app-footer-tech {
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
}
/* Narrow columns (the auth pages): one centred stack instead of two ends. */
.app-footer-compact { justify-content: center; text-align: center; }
.app-footer-compact .app-footer-links { justify-content: center; }

/* =====================================================================
   Legal / document pages (privacy, DPA, mentions légales, CGU, the wall)
   ===================================================================== */
.doc-page { max-width: 780px; }
/* The publisher identity table. A definition list, not a <table>: these are
   label/value pairs, and the pairing is what LCEN asks to be legible. Two
   columns where there is room, stacked where there is not. */
.legal-id { display: grid; grid-template-columns: 1fr; gap: .1rem .9rem; margin: 0 0 1.5rem; }
@media (min-width: 576px) { .legal-id { grid-template-columns: 13rem 1fr; } }
.legal-id dt {
  font-weight: 500; color: var(--ink-muted); font-size: var(--fs-sm);
  padding-top: .35rem;
}
.legal-id dd {
  margin: 0; color: var(--ink); padding-top: .35rem;
  padding-bottom: .35rem; border-bottom: 1px solid var(--line-soft);
}
@media (max-width: 575.98px) { .legal-id dt { padding-top: .6rem; } .legal-id dd { border-bottom: 0; } }
.doc-page h1 { font-weight: 600; font-size: 1.5rem; line-height: 1.25; letter-spacing: -.018em; }
.doc-page h2 { font-size: 1.15rem; margin-top: 2rem; }
.doc-page h3 { font-size: 1rem; margin-top: 1.4rem; }
.doc-page p, .doc-page li { color: var(--ink-muted); }
.doc-page table { font-size: var(--fs-md); }

/* =====================================================================
   Motion
   ===================================================================== */
.spin { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
.spinner-border { --bs-spinner-border-width: .18em; color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .spin, .dot-busy, .spinner-border { animation: none; }
  * { transition-duration: .01ms !important; }
}

/* =====================================================================
   RTL (§30f). i18n.js swaps Bootstrap for its .rtl build and sets dir on
   <html>; these are the pieces Bootstrap cannot know about.
   ===================================================================== */
[dir="rtl"] .u-display,
[dir="rtl"] .auth-brand h1,
[dir="rtl"] .doc-page h1 {
  /* Arabic sets loose and needs the room; the latin faces' negative tracking
     would jam the joins. */
  letter-spacing: 0;
  line-height: 1.5;
}
[dir="rtl"] .u-eyebrow { letter-spacing: 0; }
[dir="rtl"] .connect { background: radial-gradient(120% 140% at 0% 0%, rgba(176, 86, 58, .26) 0%, rgba(176, 86, 58, 0) 55%), var(--ink); }
[dir="rtl"] .auth-brand { background: radial-gradient(115% 130% at 0% 0%, rgba(176, 86, 58, .30) 0%, rgba(176, 86, 58, 0) 58%), var(--ink); }
/* The connector URL, the client id and `orcha start` are latin strings
   inside an RTL page — pin their direction so they never get reordered.

   `.u-ltr` extends the same rule to FIELDS whose CONTENT is always latin even
   when the interface is Arabic: the sign-in username (an email or login), the
   password, and the type-your-login confirmation on /closing. Left to inherit
   RTL they get a caret that starts on the right and bidi reordering that walks
   the `@` and the dots of an address around as you type — the value is still
   stored correctly, which is what makes it so confusing to look at. A password
   field shows dots, but its caret jumps the same way and Arabic-speaking users
   were typing into a box that behaved backwards.

   ⚠ Judgement, not a blanket: apply `.u-ltr` only where the content CANNOT be
   Arabic. A company name, a street or a contact name in the wizard genuinely
   may be, and pinning those would be the same bug pointed the other way. */
[dir="rtl"] .u-mono, [dir="rtl"] .copyfield input, [dir="rtl"] .say-it,
[dir="rtl"] code, [dir="rtl"] .u-ltr {
  direction: ltr; text-align: start; unicode-bidi: isolate;
}

/* ============================================================== EDA figures ==
   The three headline numbers on a design card and on a build row (§7): did the
   clock target hold, how big is it, how many signoff violations.

   ⚠ THEY ARE A ROW OF FIGURES, NOT A TABLE. A chip engineer scans these the way
   a dashboard is scanned — value first, label small above it — and there are
   exactly three, forever. A table would give them column furniture they do not
   need and would wrap badly on a phone, where they stack instead.

   `is-good` / `is-bad` tint the VALUE only. The absence of a tint is meaningful:
   a metric the flow did not report is neutral grey, never green — an unmeasured
   check must not look like a passed one. */
.eda-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.eda-figure {
  display: flex;
  flex-direction: column;
  min-width: 6.5rem;
}
.eda-figure-label {
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.eda-figure-value {
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.25;
  font-variant-numeric: tabular-nums;
}
.eda-figure-sub { font-size: var(--fs-xs); color: var(--ink-muted); }
.eda-figure.is-good .eda-figure-value { color: var(--bs-success, #198754); }
.eda-figure.is-bad  .eda-figure-value { color: var(--bs-danger, #dc3545); }

/* ============================================================== the render ==
   The KLayout render of the layout — the best visual this product has (§7).

   A chip layout is dense and roughly square, so it is given room and a neutral
   ground rather than being squeezed into a thumbnail. `image-rendering: pixelated`
   is deliberate: at 1000px wide (KLAYOUT_RENDER_RESOLUTION's default) a zoomed
   layout smoothed by the browser turns routing tracks into mush, and the tracks
   are the thing worth looking at. */
.eda-render {
  margin: 0;
  text-align: center;
}
.eda-render img {
  max-width: 100%;
  height: auto;
  background: var(--surface-muted);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  image-rendering: pixelated;
}
.eda-render figcaption {
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  margin-top: .4rem;
}
