/* NIX — the Odoo-native frontend
   ══════════════════════════════════════════════════════════════════════════
   Everything Odoo renders through website.layout wears this: /web/login, the
   signup and reset forms, the whole of /my, and the blog. It is the last
   sheet linked on those pages and it has to be — Odoo's own bundle comes
   after ours and styles by id, so the rules that must win say themselves at
   id weight. That is why nearly every selector here starts with #wrapwrap.

   In the order they appear:

     the wrapper           #wrapwrap made transparent to the flex column
                           base.css expects, or the locked body leaves the
                           page unscrollable
     theme parity          Bootstrap's own grounds knocked back onto the
                           tokens, once, at a weight that survives load order
     the sign-in page      the form Odoo ships, the card we put it in, and the
                           drop beside it. One place now; it was three
     the account           what is left of a second layout: a rule, the way
                           out, an avatar menu
     /my/store             the marketplace catalogue as cards
     notifications         the panel that slides from behind the rail
     /my/*                 account, security, plan, devices, payment method
     /my/support           the ticket door and the ticket itself

   THERE IS ONE RAIL NOW, AND IT IS shell.css's. This sheet used to carry a
   second one — .nix-apprail, 58px, always out, no seam and no way to close
   it — beside a second shell, #wrapwrap.nix-shell, a flex row with its own
   padding and its own panel at its own radius. Two layouts one letter apart,
   and the account was the half that never got any of the other's work: it
   could not be resized, collapsed to a strip, or sent off the page.

   The account wears .nav-shell like every other page here. .nix-account is
   what replaces .nix-shell and it is NOT a layout — it is the marker the
   rules below hang off so that they style what is inside the panel on an
   account page and nowhere else. Anything about the shell itself, the rail,
   its seats or its states belongs in shell.css, and putting it back here is
   how this sheet grew a second one the first time.
   ══════════════════════════════════════════════════════════════════════════ */
#wrapwrap {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}

/* THE 42rem READING COLUMN IS GONE. `:not(.nav-shell)` is the SIGNED-IN
   shell — the layout writes exactly one of the two classes on #wrapwrap — so
   a cap written for the login card was being applied to the account, where
   the shell's own `max-width: none; margin: 0` further down undid it anyway.
   What it actually contributed was `width: 100%`, which a panel's children
   have in any case. */

/* The landing reveals these with body.is-ready once its preloader hands
   over; the portal has no preloader, so nothing would ever set the class
   and the brand + copyright sit at opacity:0 forever. */
#wrapwrap .site-head__brand {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------------------ *
 * Theme parity. Odoo's own stylesheets load after ours and repaint
 * body white, so dark mode turned the logo white (tokens.css inverts it)
 * on a page that never went dark — an invisible brand. Everything here
 * reads the tokens, which flip on html[data-theme], so no rule needs a
 * per-theme copy of itself. Specificity (html body, #wrapwrap) outranks
 * the bootstrap rules regardless of load order.
 * ------------------------------------------------------------------ */

html body {
  background: var(--nix-paper);
  color: var(--nix-text);
  font-family: var(--font-sans);
}

#wrapwrap {
  background: var(--nix-paper);
  color: var(--nix-text);
  /* THE FIELD FILL, ON THE WRAPPER RATHER THAN ON ONE CARD. Every rule that
     spends it is written against #wrapwrap — the fields, the list rows, the
     password reveal — and it was declared on the sign-in card alone, so a
     reset or signup form rendered anywhere else resolved it to nothing. Those
     fields also carry `border: none`: an invisible input on a white page. */
  --fill: color-mix(in srgb, var(--nix-text) 7%, var(--nix-surface));
}

#wrapwrap .form-control,
#wrapwrap .list-group-item,
#wrapwrap .o_show_password {
  background: var(--nix-surface);
  color: var(--nix-text);
  border-color: var(--nix-rule);
}

#wrapwrap .form-control::placeholder {
  color: var(--nix-text-soft);
}

/* bootstrap declares this one !important, so matching it is the only way */
#wrapwrap .text-muted {
  color: var(--nix-text-soft) !important;
}

/* Email + password only: the '- or -', Odoo.com OAuth and passkey block. */
#wrapwrap .o_login_auth {
  display: none;
}

/* Odoo ships the form d-none and lets the user-switch owl component
   reveal it once the JS bundle arrives — an empty card in the meantime.
   No remembered-user switch on this portal, so the form just shows.
   Scoped to any form in the card: login, reset password, signup.
   !important against !important: bootstrap's d-none carries one too. */
#wrapwrap .oe_website_login_container form {
  display: block !important;
  width: 100%;
}

#wrapwrap owl-component:empty {
  display: none;
}

/* Full width of the card for every row of every card form. */
#wrapwrap .oe_website_login_container form .mb-3,
#wrapwrap .oe_website_login_container form .o_caps_lock_warning,
#wrapwrap .oe_website_login_container form .input-group,
#wrapwrap .oe_website_login_container form .form-control,
#wrapwrap .oe_website_login_container form .oe_login_buttons,
#wrapwrap .oe_website_login_container form .btn-primary {
  width: 100%;
}

/* 'Back to Login' under the button: centered, in the text color. On the
   reset page it is a bare <a> in a justify-content-between row (bootstrap
   utility, shipped !important — matching it is the only way). */
#wrapwrap .oe_login_buttons .btn-link,
#wrapwrap .oe_login_buttons a {
  display: block;
  margin-top: 8px;
  text-align: center;
  color: var(--nix-text);
  text-decoration: none;
  font-size: 12px;
}

#wrapwrap .oe_login_buttons .d-flex {
  justify-content: center !important;
}

/* One signup door, ours. If Free sign up is switched on, auth_signup adds
   its own 'Don't have an account?' next to the permanent Create account. */
#wrapwrap .oe_login_buttons a.btn-link:not(.nix-login__signup) {
  display: none;
}

/* NO GLOW ON A POINTER, A RING ON A KEYBOARD. Bootstrap's focus is a fat
   blue halo and it had to go — but `outline: none` on :focus-visible went
   with it, and that left the sign-in page, where typing is the entire
   interaction, with no focus state at all: the :focus background below is
   the same --fill the field already rests on. Every other control on this
   site draws --focus-ring; so does this one now. */
#wrapwrap .form-control:focus,
#wrapwrap .btn:focus {
  outline: none;
  box-shadow: none;
  border: none;
}

#wrapwrap .form-control:focus-visible,
#wrapwrap .btn:focus-visible,
#wrapwrap .o_show_password:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

#wrapwrap .form-control:focus {
  background: var(--fill);
  color: var(--nix-text);
}

/* Autofill paints its own tint (the violet wash on the email field);
   inset-shadow over it is the one mechanism browsers honour. */
#wrapwrap input:-webkit-autofill,
#wrapwrap input:-webkit-autofill:hover,
#wrapwrap input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--nix-surface) inset;
  -webkit-text-fill-color: var(--nix-text);
  caret-color: var(--nix-text);
}

/* Except inside the input group, where the field shares the row with the
   eye button and flexes instead. */
#wrapwrap .input-group .form-control {
  width: auto;
  flex: 1;
}

/* ------------------------------------------------------------------ *
 * Sign-in card, after Agent Base's login: one 360px panel centered in
 * the viewport, a masked mark, a light display title, fill fields with
 * no hairline, and an ink button that flips contrast with the theme.
 * --fill is the one derived tone: a small step of text over surface,
 * which lands close to Agent Base's fill in both themes.
 * ------------------------------------------------------------------ */

/* THE LOGIN, named by what is in it. :not(.nix-account) meant "not signed
   in", which was the same thing as "the login page" right up until the blog
   started rendering through this layout as well — and then it centred the
   blog as a column and, above 920px, laid it out as a ROW: posts down one
   side, foot down the other. :has() says the actual condition. */
#wrapwrap > main.page:has(> .oe_website_login_container) {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* The child selector matters: it has to outrank the layout's own idea of
   what a child of the scroller is wide. */
#wrapwrap > main.page > .oe_website_login_container {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 32px;
  background: var(--nix-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift-2);
}

#wrapwrap .nix-login__title {
  margin: 0 0 24px;
  font-size: 26px;
  font-weight: 300;
  text-align: center;
  color: var(--nix-text);
}

/* One label voice on every card: login uses .form-label, the reset and
   signup forms use .col-form-label — same size, color and spacing. */
#wrapwrap .form-label,
#wrapwrap .col-form-label {
  margin-bottom: 8px;
  padding: 0;
  font-size: 16px;
  line-height: 1.4;
  color: var(--nix-text);
}

#wrapwrap .form-control,
#wrapwrap .o_show_password {
  background: var(--fill);
  border: none;
  border-radius: var(--radius-sm);
  padding: 12px;
  font-size: 13px;
}

#wrapwrap .input-group .form-control {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

#wrapwrap .o_show_password {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  color: var(--nix-text-soft);
}

#wrapwrap .btn-primary {
  margin-top: 12px;
  padding: 12px;
  font-size: 13px;
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  border: none;
  border-radius: var(--radius-sm);
}

/* TOWARD THE GROUND, NOT TO A FIXED GREY. --nix-ink-soft is a mid grey in
   light and a light grey in dark, so hovering the one button on the sign-in
   page took its label from the darkest ink on the page to under the floor — under AA, and it read as the
   button going disabled at the moment it was pressed. Mixing toward the paper
   darkens in light and lightens in dark, and both stay well clear of the floor. */
#wrapwrap .btn-primary:hover {
  background: color-mix(in srgb, var(--nix-ink) 82%, var(--nix-paper));
  color: var(--nix-on-ink);
}

#wrapwrap .btn-link {
  font-size: 12px;
  color: var(--nix-text-soft);
  text-decoration: none;
}

#wrapwrap .btn-link:hover {
  color: var(--nix-text);
}

#wrapwrap .list-group {
  gap: 8px;
}

#wrapwrap .list-group-item {
  background: var(--fill);
  border: none;
  border-radius: var(--radius-sm);
  padding: 12px;
  font-size: 13px;
  color: var(--nix-text);
}

#wrapwrap .list-group-item:hover {
  background: color-mix(in srgb, var(--nix-text) 12%, var(--nix-surface));
}

/* THE DROP BESIDE THE CARD. On a phone the column keeps its order — card
   first, the drop below it — and it only appears beside the form once there
   is room for a second column.

   Scoped to .nav-shell, which is the class the layout puts on #wrapwrap for a
   visitor who is not signed in, so this is "the public login" said in markup.
   Unscoped it tied on specificity with the shell's own
   `#wrapwrap.nix-account > main.page > *`, and which of them won was decided by
   which one happened to be written last in this file — a thing that changes
   the moment anybody tidies it. */
#wrapwrap.nav-shell > main.page > .nix-login-orb {
  position: relative;
  width: min(260px, 70vw);
  max-width: none;
  aspect-ratio: 1 / 1;
  margin: 32px auto 0;
  flex: none;
}

/* 900.02, not 901: at any fractional width between the two — browser zoom,
   Windows display scaling — neither this nor the max-width: 900px rules
   matched, and the card was stacked while the drop still had its phone
   width. */
@media (min-width: 900.02px) {
  #wrapwrap > main.page:has(> .oe_website_login_container) {
    /* A GRID, AND ITS FIRST ROW IS THE WHOLE SCREEN.

       Two things had to be true at once and a flex row could do neither
       cleanly. The first: main.page holds the card, the drop AND the
       four-column foot the layout puts inside every page, so a plain row laid
       all three side by side — card, orb, footer, 72px apart — and pushed the
       card off the left edge of the window.

       The second: the foot belongs BELOW THE FOLD. A reader arriving at the
       sign-in page should see a sign-in page, not a sign-in page with a site
       map under it; the foot is something you go looking for by scrolling.
       `100%` here is the panel's own height, so the pair sits in a screen of
       its own and the foot begins exactly where that screen ends. */
    display: grid;
    /* FOUR TRACKS FOR TWO THINGS. The pair is centred by the two 1fr columns
       on either side of it, NOT by justify-content — a centred grid is only
       as wide as its columns, so the foot spanning `1 / -1` spanned the card
       plus the drop and nothing more: four columns of links squeezed into a
       420px card's width, wrapping to three and then to one, standing off to
       the left of a page whose every other element is full width.

       With the outer tracks there, the grid fills the panel, the pair still
       sits in the middle of it, and the foot below spans the whole thing. */
    grid-template-columns: 1fr auto auto 1fr;
    grid-template-rows: 100% auto;
    align-items: center;
    column-gap: 72px;
  }

  #wrapwrap > main.page > .oe_website_login_container { grid-area: 1 / 2; }

  #wrapwrap.nav-shell > main.page > .nix-login-orb { grid-area: 1 / 3; }

  #wrapwrap > main.page:has(> .oe_website_login_container) > .index-foot {
    grid-column: 1 / -1;
    width: 100%;
  }
  #wrapwrap > main.page > .oe_website_login_container {
    margin: 0;
    flex: none;
  }
  #wrapwrap.nav-shell > main.page > .nix-login-orb {
    width: min(360px, 34vw);
    margin: 0;
  }
}

/* ------------------------------------------------------------------ *
 * The drop, beside the card. components.css positions .assist__orb absolutely
 * for the guide overlay; here it is an ordinary flex sibling, shown only
 * when there is room for a second column — never stacked above the form.
 * ------------------------------------------------------------------ */

/* Odoo leaves empty editor drop-zones around the form; as flex items in
   the row layout they would claim column space. */
#wrapwrap .oe_structure:empty {
  display: none;
}

/* ------------------------------------------------------------------ *
 * The account, INSIDE the site's own shell.
 *
 * What used to be here was a second layout: #wrapwrap.nix-shell as a flex
 * row with a fixed 58px .nix-apprail beside it. It was written before the
 * public shell could do anything but open and shut, and it is the reason
 * the account was the one part of this site whose navigation could not be
 * resized, collapsed to a strip or sent off the page — every improvement
 * to the other one had to be copied here by hand, and none ever was.
 *
 * The grid, the three states, the rail's ground and radius, the seats, the
 * strip, the tooltips, the seam and the ring all come from shell.css now.
 * What is left below is what is genuinely this leg's own: a rule, the way
 * out, an avatar menu, and a notifications panel that pushes the page across.
 * ------------------------------------------------------------------ */

/* One measure all round, and small: the title's centre stays level with the
   rail logo's, and the content hugs the panel's left edge. The ground and the
   radius are the shell's — they used to be declared here as well, at
   --radius-lg against the shell's --radius-xl, so the account's panel was a
   different object from every other panel in the product. */
#wrapwrap.nix-account > main.page {
  padding: 12px;
}

/* The one-line landing footer has no seat in the shell. */
#wrapwrap.nix-account .foot {
  display: none;
}

/* ── THE SITE MAP IS NOT PART OF THE ACCOUNT SCREEN ───────────────────────
   Two devices, three plan cards or one card form do not fill a panel, and
   the foot is the last child of a flex column: with nothing above it tall
   enough, it climbed up and sat in the middle of the empty panel — a
   four-column site map with Blog, Contact sales and the social icons, in
   the reader's eye on every account page, at all times, under a list of two
   machines.

   The page's own block claims the whole panel first, so the foot starts
   exactly where the screen ends and a reader who wants it goes looking. It
   is the same answer the blog got next door in shell.css, for the same
   reason, and it is written here because these pages have no #wrap of their
   own to hang it on — /my/devices is a bare div, and Odoo's own
   /my/payment_method is whatever the payment module wraps it in.

   :first-child, not :not(.index-foot): several of these pages render two or
   three blocks and giving each of them a full panel would make a two-line
   page three screens tall. The foot is appended last by the layout, so the
   first child is the page every time.

   flex-grow with no min-height would only take the space LEFT OVER after the
   foot has been laid out — which is the state being fixed, foot on screen at
   the bottom of a mostly empty panel. The 100% is what pushes it past the
   fold; the grow is what keeps a short page's own ground filling the panel
   rather than ending under its last card. */
#wrapwrap.nix-account > main.page > *:first-child {
  flex: 1 0 auto;
  min-height: 100%;
}

/* ── AN ACCOUNT TITLE IS LEFT, SAID ONCE AND SAID QUIETLY ─────────────────
   The sign-in card centres .nix-login__title, and every account page that
   reuses that class had its own rule undoing it — Devices, Plan and Store
   each carried `text-align: left` beside their margin and weight. Three
   copies of one decision, and each of them was a three-class selector, which
   made them heavier than the phone's centring rule below: on a phone every
   title in this panel was still hard left while the subtitle under it, which
   had no such override, sat in the middle. The page read as two halves of two
   different designs.

   One rule, and deliberately as light as it can be said. Two classes and no
   element, so anything that wants to centre a title — either state below —
   outranks it without having to name a page. */
#wrapwrap.nix-account .nix-login__title {
  text-align: left;
}

/* ── WITH THE RAIL AWAY, THE HEADING MOVES OFF THE RING ───────────────────
   Sent away, the rail leaves the ring at the panel's top-left — and the
   account pages open with an h1 at exactly that corner, so "Devices" was
   drawn under it. The heading centres in the panel instead: same line it was
   always on, no row inserted above it, nothing else on the page moved. The
   ring keeps the corner because that is where it has to be to be found.

   The line under the title comes with it. They are one header, and a centred
   heading over a left-aligned subtitle is two decisions where there was one —
   the three below are the three pages that have such a line.

   AND THIS PANEL HAS FOUR KINDS OF TITLE, because only half of the account
   is this module's. Naming the four classes our own templates write moved
   Devices and Plan and left the rest under the ring; naming the h1 moved
   Store and Support with them and still left the Odoo pages, because NONE
   OF THEM HAS AN h1:

     our pages          h1.nix-login__title
     Odoo's portal      h3 in .o_portal_wrap  (/my, /my/security)
     Odoo's payment     <p class="mb-3 h4">   (/my/payment_method)
     Odoo's doc lists   .navbar-brand in .o_portal_navbar
                        (/my/quotes, /my/orders, /my/invoices)

   THE FOURTH ARRIVED WITH THE RAIL ROWS THAT REACH IT. The three document
   lists were always live — they are sale's and account's own routes — but
   nothing in this account linked them, so no reader ever saw one and the
   title they carry was never anybody's problem. Drawing the Business
   Services rows made them reachable and made this list short by one.

   It is a <span class="navbar-brand"> inside portal.portal_searchbar, which
   is one template shared by every list Odoo's portal ships: quotations,
   orders, invoices and — the moment `project` is declared — projects and
   tasks. So this line is the last one this inventory needs.

   The last one is the one that was reported twice. It is a paragraph wearing
   a heading's class, inside payment.form, and it is only the page's title
   when it is the FIRST thing in that form — with a card already saved, an
   "Your payment methods" h4 stands above it and that is the title instead.
   So the selector is the form's first block's first child, whichever of the
   two it turns out to be, and a section header further down the page stays
   where it is. The three rules above it already treat "the h3 in o_portal_wrap"
   as the page title; this follows them rather than inventing a second answer.

   Only in this state. With a rail beside it there is no ring over the panel
   and nothing to move away from, so the heading stays where every other page
   in the product puts it. */
body.is-shut:not(.is-nav) #wrapwrap.nix-account > main.page h1,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .o_portal_wrap h3,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .o_portal_navbar .navbar-brand,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .wrap #o_payment_form_options > div:first-child > :first-child,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .nix-devices__plan,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .nix-plans__current,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .nix-pay__lede,
body.is-shut:not(.is-nav) #wrapwrap.nix-account .nix-store__lede {
  text-align: center;
}

/* .nav-rail__sep MOVED TO shell.css, and it had to: the public rails carry one
   now too, and four of the five of them are standalone documents that never
   link this sheet. A shared part styled in the leg-specific sheet is a part
   that is only styled on one leg. */

/* .nav-rail__back MOVED TO shell.css, and for the same reason .nav-rail__sep
   went before it: /agent-base drills its own sections into the rail behind a
   back row of exactly this kind, and that page is a standalone document that
   never links this sheet. */

/* The three seats that are not destinations: store, bell, account. */
.nav-rail__foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* THE `position: relative` THAT WAS HERE IS GONE WITH WHAT NEEDED IT. It
     existed so the avatar popover could be absolute against this block; that
     box is fixed and hangs off <body> now — the rail is overflow-x: hidden
     and clips anything that opens past its right edge. Nothing else inside
     this block is positioned: the notifications panel is fixed against the
     viewport, not against the foot. */
}

/* button.nav-rail__login AND .nix-popover BOTH MOVED TO shell.css. The account
   seat was a <button> only here while the public rails drew an <a>; it is one
   partial now, so every rail's seat is a button — and four of the five rails
   are standalone documents that never link this sheet. Undressing the button
   on one leg is the same seat rendered as two different controls, which is
   what it looked like. Same reason .nav-rail__sep and .nav-rail__back went
   before them. */

/* ------------------------------------------------------------------ *
 * /my/store — the marketplace catalogue as cards. Same page furniture
 * as /my/devices (left-aligned title, 400 weight) so the two read as
 * two pages of one portal rather than two designs.
 * ------------------------------------------------------------------ */

#wrapwrap.nix-account .nix-store .nix-login__title {
  margin: 0 0 8px;
  font-weight: 400;
}

#wrapwrap.nix-account .nix-store__lede {
  margin: 0 0 16px;
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-store__search {
  max-width: 22rem;
  margin: 0 0 24px;
}

#wrapwrap.nix-account .nix-store__search input {
  width: 100%;
  padding: 10px 12px;
  border: none;
  border-radius: 10px;
  background: var(--nix-surface);
  color: var(--nix-text);
  font: inherit;
}

#wrapwrap.nix-account .nix-store__search input::placeholder {
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-store__empty {
  color: var(--nix-text-soft);
}

/* auto-fill and not auto-fit: with two listings, auto-fit stretches them
   across the whole panel and the store looks like a table of two rows.
   auto-fill keeps the card at its own size and leaves the rest empty,
   which is what a catalogue with two things in it actually is. */
#wrapwrap.nix-account .nix-store__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 8px;
  max-width: 62rem;
}

/* A CARD ON A PANEL NEEDS AN EDGE, and moving the panel onto --nix-panel
   took the one it had, and white on near-white is less of a step
   than the 5.5% grey literal this commit series removed for being invisible.
   The lift is the edge in light and is already `none` in dark, where the two
   grounds are far enough apart on their own. */
#wrapwrap.nix-account .nix-store__card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

#wrapwrap.nix-account .nix-store__name {
  margin: 0;
  font-size: 15px;
  font-weight: 400;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-store__by,
#wrapwrap.nix-account .nix-store__installs {
  margin: 0;
  font-size: 12px;
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-store__summary {
  margin: 4px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--nix-text-soft);
}

/* margin-top:auto so the price sits on the card's floor whatever the
   summary's length — a row of cards with the price at a different height
   in each is what an uneven grid reads as. */
#wrapwrap.nix-account .nix-store__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: auto;
  padding-top: 12px;
}

#wrapwrap.nix-account .nix-store__price {
  font-size: 13px;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-store__note {
  margin: 24px 0 0;
  font-size: 12px;
  color: var(--nix-text-soft);
}

/* THE NOTIFICATIONS PANEL IS GONE, styles and all. It was the app's
   .notif-panel measure for measure — a frame pinned at the rail's right
   edge with the card sliding out from behind it, and the shell shifting
   across rather than the panel covering the page. Nothing produced a
   notification for it to show, so the bell, the panel, its script and
   these rules were removed together. The shell's `> main.page` had a
   margin-left transition that existed only to make room for it; that
   went too, because a transition on a margin nothing sets is a frame of
   easing on any future layout change to the same property. */

/* THE SECOND TOOLTIP IS GONE with the script that drew it. js/rail.js draws
   .rail-tip for every seat in a .nav-rail, off the label the markup carries
   anyway, and this rail is a .nav-rail now. Two bubbles for one strip of
   icons is two places for a label to be wrong.

   The dark-logo rule went with it too: the mark in this rail is
   .site-head__logo, which tokens.css already flips by name. */

/* ------------------------------------------------------------------ *
 * Forms inside the shell panel (/my/account and friends): the same
 * fill fields and ink button the sign-in card wears, and none of the
 * website theme's accent color on buttons or links.
 * ------------------------------------------------------------------ */

#wrapwrap.nix-account .form-control,
#wrapwrap.nix-account .form-select {
  background: color-mix(in srgb, var(--nix-text) 7%, var(--nix-paper));
  color: var(--nix-text);
  border: none;
  border-radius: var(--radius-sm);
  padding: 12px;
  font-size: 13px;
}

#wrapwrap.nix-account .form-select {
  padding-right: 32px;
}

#wrapwrap.nix-account .btn-primary {
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  border: none;
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  font-size: 13px;
}

/* The same mix the sign-in button takes, for the same reason: --nix-ink-soft
   is a mid grey in light, and white on it is under the floor — under AA, on every
   Save and Confirm in /my. */
#wrapwrap.nix-account .btn-primary:hover {
  background: color-mix(in srgb, var(--nix-ink) 82%, var(--nix-paper));
  color: var(--nix-on-ink);
}

#wrapwrap.nix-account .btn-secondary,
#wrapwrap.nix-account .btn-light,
#wrapwrap.nix-account .btn-outline-primary,
#wrapwrap.nix-account .btn-outline-secondary,
#wrapwrap.nix-account .btn-link {
  background: color-mix(in srgb, var(--nix-text) 7%, var(--nix-paper));
  color: var(--nix-text);
  border: none;
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  font-size: 13px;
  text-decoration: none;
}

/* EVERY LINK IN THE ACCOUNT IS TEXT, EXCEPT IN THE STORE. Odoo paints links
   with its own link colour, which is the one thing on a portal page that is
   not this site's ink — hence the blanket rule. The store is the page in /my
   that is NOT Odoo's markup: it is nix_website.marketplace_body, the same
   template /marketplace draws, and it colours its own links — the price
   button's label is paper on ink, and this rule painted it ink on ink, so the
   one button in the shop window read as an empty black pill. Measured: rgb(26,
   26, 26) on rgb(26, 26, 26).

   :has, and the same :has shell.css already leans on. A class on <main> would
   have meant a conditional attribute in the layout for a rule that is about
   what the page CONTAINS, and the answer is in the markup either way. */
/* AND NOT A BUTTON. Same defect as the store's price pill, one page over:
   `a.btn-primary` on the quotation is paper-on-ink by its own rule, and this
   one repainted its label --nix-text — which in dark IS the button's own
   background. Measured on /my/orders: rgb(244, 243, 241) on rgb(244, 243,
   241), so "Accept & Sign" was a blank white pill and the one control the
   whole page exists for could not be read.

   :not(.btn) rather than another id-heavy override, because every .btn class
   in here already sets its own colour — this rule has nothing to say about
   any of them. */
#wrapwrap.nix-account main.page:not(:has(.mk-hero, .mk-sec)) a:not(.btn) {
  color: var(--nix-text);
}

/* Belt to the template's braces: if a build still renders the details
   sidebar (avatar, email, Edit information) or its offcanvas twin, it
   stays invisible and the content column takes the row. */
#wrapwrap.nix-account .row > .d-lg-flex.col-lg-4,
#wrapwrap.nix-account #accountOffCanvas,
#wrapwrap.nix-account [data-bs-target="#accountOffCanvas"] {
  display: none !important;
}

#wrapwrap.nix-account .o_portal_content {
  width: 100%;
}

/* Inside the shell the panel is the page: content hangs off its top-left
   like the app's Agents view, not down a centered website column. The
   42rem column, the login's vertical centering and bootstrap's .container
   all step aside — and the rows lose their negative margins, which were
   pushing cards past the panel's right edge once the container was gone. */
#wrapwrap.nix-account > main.page {
  justify-content: flex-start;
}

#wrapwrap.nix-account > main.page > * {
  max-width: none;
  margin: 0;
}

#wrapwrap.nix-account .o_portal_wrap .row {
  margin-left: 0;
  margin-right: 0;
}

/* The rail already navigates everywhere the cards pointed: the account
   home keeps only its title. */
#wrapwrap.nix-account .o_portal_docs {
  display: none;
}

/* Bootstrap indents the cards through three layers of column padding
   (content col 15px, category 4, card 4) while the title sits at the
   panel edge. The category becomes a grid of tiles instead — flush
   left with the title, the way the app lays its tiles. */
#wrapwrap.nix-account .o_portal_wrap .wrapper,
#wrapwrap.nix-account .o_portal_wrap .o_portal_content {
  padding-left: 0;
  padding-right: 0;
}

#wrapwrap.nix-account .o_portal_category {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 12px;
  padding: 0;
  margin: 24px 0 0;
}

#wrapwrap.nix-account .o_portal_index_card {
  padding: 0;
  width: auto;
  max-width: none;
}

/* Not this portal's business either: orders, invoices and addresses
   stay off the account home. */
#wrapwrap.nix-account .o_portal_index_card:has(a[href^="/my/orders"]),
#wrapwrap.nix-account .o_portal_index_card:has(a[href^="/my/invoices"]),
#wrapwrap.nix-account .o_portal_index_card:has(a[href^="/my/addresses"]) {
  display: none;
}

/* /my/account's address form: unconstrained it spreads col-lg-8 across
   the whole panel and pushes Zip off the edge. One 44rem column, the
   outer col flattened, and the form's own rows keep their bootstrap
   gutters so paired fields (City/Zip) still sit side by side flush. */
#wrapwrap.nix-account .o_customer_address_fill {
  max-width: 44rem;
}

#wrapwrap.nix-account .o_customer_address_fill > .row > [class*="col-"] {
  width: 100%;
  padding: 0;
}

#wrapwrap.nix-account .o_customer_address_fill form .row {
  margin-left: -12px;
  margin-right: -12px;
}

/* Not the portal's business: VAT and the invoice-delivery preferences
   (Receive invoices / Electronic format) stay out of the Profile form.
   The invoice rows carry m-0, which keeps :has() off the form's own
   outer row. */
#wrapwrap.nix-account #div_vat,
#wrapwrap.nix-account .row.m-0:has(select[name="invoice_sending_method"]),
#wrapwrap.nix-account .row.m-0:has(select[name="invoice_edi_format"]) {
  display: none;
}

/* The empty breadcrumb strip (o_portal container mt-3) sat above #wrap
   on the details page and pushed Profile below the other titles. */
#wrapwrap.nix-account > main.page > .o_portal.mt-3 {
  display: none;
}

/* /my/security: password change only. Two-factor and passkeys stay out
   (their sections carry the ids of their own buttons in either state),
   the Go Back chip goes — the rail is the way around — and the header
   row loses its my-3 so the title sits level with the other pages'. */
#wrapwrap.nix-account .o_portal_security_body section:has(#auth_totp_portal_enable),
#wrapwrap.nix-account .o_portal_security_body section:has(#auth_totp_portal_disable),
#wrapwrap.nix-account .o_portal_security_body section:has(#portal_passkey_add),
#wrapwrap.nix-account .o_portal_security_body #totp_wizard_view,
#wrapwrap.nix-account .o_portal_security_body a[title="Go Back"],
#wrapwrap.nix-account .o_portal_security_body section[name="portal_deactivate_account"] {
  display: none;
}

#wrapwrap.nix-account .o_portal_security_body > .d-flex.my-3 {
  margin: 0 0 24px !important;
  /* AND IT STOPS BEING A ROW. Odoo puts the title in a d-flex beside a Go
     Back chip; the chip is hidden above, and what is left is a lone flex
     item — a box the width of the words "Connection & Security" and not one
     pixel more. Centring text inside that box moves nothing, which is why
     this title stayed under the ring through two commits that centred it.
     As a block it is as wide as the panel, and centre means the panel.

     !important, and that is the whole reason this took a third commit:
     BOOTSTRAP'S DISPLAY UTILITIES SHIP !important. `.d-flex` is
     `display: flex !important`, so a plain `display: block` here — however
     specific the selector — never won, and the title carried on being a
     flex item the width of its own words. The margin above this line has
     carried !important for the same reason since the day it was written. */
  display: block !important;
}

/* THE READING COLUMN IS THE SECTIONS, NOT THE PAGE. The cap used to be on
   the body, which is the box the title lives in — so "centred" was centred
   in 44rem of a much wider panel. It moves down one level: every section
   keeps the same 44rem it had, the title keeps the panel, and nothing on
   this page changes width. */
#wrapwrap.nix-account .o_portal_security_body {
  width: 100% !important;
  max-width: none;
}

#wrapwrap.nix-account .o_portal_security_body > section,
#wrapwrap.nix-account .o_portal_security_body > .alert {
  max-width: 44rem;
}

/* /my/payment_method: same seat as the rest. Its wrapper is a bare
   .wrap (not o_portal_wrap), the breadcrumb goes, the centered column
   docks left at 44rem, and the form's own 'Payment method' header
   dresses like the other page titles. */
#wrapwrap.nix-account main.page > .wrap > .container {
  max-width: 100%;
  margin: 0;
  padding: 0;
}

#wrapwrap.nix-account .wrap ol.breadcrumb {
  display: none;
}

#wrapwrap.nix-account .wrap .row {
  margin: 0;
}

#wrapwrap.nix-account .wrap .row.justify-content-center {
  justify-content: flex-start !important;
}

/* THE SAME MOVE AS THE SECURITY PAGE, one level down again. The cap was on
   the column, and the column is the box the payment form's own header sits
   in — so the title centred over 44rem while the panel it was supposed to be
   centred in was twice that, which is the "not quite in the middle" that was
   reported. The column takes the whole panel, the two things that need a
   reading measure keep it, and the card's width on screen is unchanged. */
#wrapwrap.nix-account .wrap .row.justify-content-center > [class*="col-"] {
  width: 100%;
  max-width: none;
  padding: 0;
}

/* .justify-content-end, not `> .d-flex`: #o_payment_form_options is a d-flex
   child of the same form and capping IT would put the header straight back
   inside 44rem — the exact bug this rule exists to undo, one selector wider. */
#wrapwrap.nix-account .wrap #o_payment_form ul.list-group,
#wrapwrap.nix-account .wrap #o_payment_form > .d-flex.justify-content-end {
  max-width: 44rem;
}

#wrapwrap.nix-account .wrap p.h4 {
  margin: 0 0 24px !important;
  font-size: 26px;
  font-weight: 400;
  color: var(--nix-text);
}

#wrapwrap.nix-account .o_portal_wrap .container {
  max-width: 100%;
  margin: 0;
  /* bootstrap's pt-3/pb-5 utilities ship !important */
  padding: 0 !important;
}

/* The page title, the app way: top-left, quiet weight, room below. */
#wrapwrap.nix-account .o_portal_wrap h3 {
  /* my-3 is a bootstrap utility and ships !important */
  margin: 0 0 24px !important;
  font-size: 26px;
  font-weight: 400;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-devices {
  padding: 0;
}

/* ── /my — THE DASHBOARD ────────────────────────────────────────────────────
   One card on an otherwise empty page, and the page is the panel: no padding
   of its own, the title where every other account page puts it, and the card
   directly under it. It is a surface on the panel — the same lift and radius
   the store's cards and the sign-in card wear — because a headline sitting on
   the panel with nothing under it would read as a page that failed to load
   rather than a page with one thing on it. */
#wrapwrap.nix-account .nix-dash {
  padding: 0;
}

/* NO text-align HERE. One rule in this sheet left-aligns an account title and
   it is deliberately light enough for the phone block to outrank it; a
   per-page copy is three classes, which no media query can beat, and that is
   how every title in this panel once sat hard left under a centred subtitle
   on a phone. */
#wrapwrap.nix-account .nix-dash .nix-login__title {
  margin: 0 0 24px;
  /* SAME TITLE AS EVERY OTHER PAGE IN THE ACCOUNT. 300 and centred, which is
     what Settings and the bar over each of its sub-pages carry; these three
     were 400 and hard left, so moving between them changed the weight and the
     side of the panel the page was titled on. */
  font-weight: 300;
  text-align: center;
}

#wrapwrap.nix-account .nix-dash__card {
  max-width: 42rem;
  padding: 24px;
  background: var(--nix-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift-1);
}

#wrapwrap.nix-account .nix-dash__title {
  margin: 0 0 8px;
  font-size: 18px;
  font-weight: 400;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-dash__text {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--nix-text-soft);
}

/* ── THE FOUR TILES, FOR A BUSINESS-SERVICES READER ────────────────────────
   The same card the "coming soon" block wears — --nix-surface, --radius-lg,
   --lift-1 — four across, and each one is the <a> itself rather than a card
   with a link inside it: the number is the door, so the whole tile takes the
   press and there is no second, smaller target to aim at.

   auto-fit and not a fixed four columns. The panel narrows with the rail out,
   and four tiles that keep their count wrap into a ragged second row of one;
   letting them fill means three and one becomes two and two, which is the
   shape the store's grid already settles into for the same reason.

   max-width matches .nix-dash__card. The tiles sit directly above that card
   and a row wider than the block under it reads as two designs stacked. */
#wrapwrap.nix-account .nix-dash__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 16px;
  max-width: 42rem;
  margin: 0 0 16px;
}

#wrapwrap.nix-account .nix-dash__stat {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 20px;
  background: var(--nix-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift-1);
  color: var(--nix-text);
  text-decoration: none;
}

/* The tile lifts on hover rather than colouring: this panel says "pressable"
   with light everywhere else, and a fifth accent on the account's front page
   would be the only one of its kind. */
#wrapwrap.nix-account .nix-dash__stat:hover {
  box-shadow: var(--lift-2);
}

#wrapwrap.nix-account .nix-dash__num {
  font-size: 30px;
  font-weight: 300;
  line-height: 1.1;
}

/* The word under the number is the quiet half: the figure is what the reader
   came for and the label only says which figure it is. */
#wrapwrap.nix-account .nix-dash__label {
  font-size: 13px;
  color: var(--nix-text-quiet);
}

#wrapwrap.nix-account .nix-devices__plan,
#wrapwrap.nix-account .nix-devices__empty {
  text-align: left;
}

#wrapwrap.nix-account .nix-devices .nix-login__title {
  margin: 0 0 8px;
  /* SAME TITLE AS EVERY OTHER PAGE IN THE ACCOUNT. 300 and centred, which is
     what Settings and the bar over each of its sub-pages carry; these three
     were 400 and hard left, so moving between them changed the weight and the
     side of the panel the page was titled on. */
  font-weight: 300;
  text-align: center;
}

/* THE LIST SITS UNDER THE TITLE, not off to its left. The measure was there
   from the start — a device row is a name and a button, and a row as wide as a
   desk puts eighty centimetres between them — but with no auto margin the
   column stood hard against the panel's left edge while the heading over it
   was centred, so the page read as two things that had nothing to do with each
   other. */
#wrapwrap.nix-account .nix-devices__list {
  max-width: 42rem;
  margin-inline: auto;
}

/* ------------------------------------------------------------------ *
 * /my/plan — the plans as tiles, the current one marked.
 * ------------------------------------------------------------------ */

#wrapwrap.nix-account .nix-plans .nix-login__title {
  margin: 0 0 8px;
  /* SAME TITLE AS EVERY OTHER PAGE IN THE ACCOUNT. 300 and centred, which is
     what Settings and the bar over each of its sub-pages carry; these three
     were 400 and hard left, so moving between them changed the weight and the
     side of the panel the page was titled on. */
  font-weight: 300;
  text-align: center;
}

#wrapwrap.nix-account .nix-plans__current {
  margin: 0 0 24px;
  color: var(--nix-text-soft);
  /* Under the title it belongs to, not off to its left. It was centred only
     with the rail away and on a phone — the two states where a rule further up
     catches it — so with the rail out it sat hard left under a centred "Plan".
     Same line every other subtitle in this account now takes. */
  text-align: center;
}

/* The landing page's .ab-price family, copied verbatim from ab01's inline
   sheet (that page is standalone and never loads this file) with one
   change: the grid docks left under the title instead of centering. */
.ab-price {
  width: min(1000px, 100%);
  margin: 24px 0 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  text-align: left;
}

.ab-price__card {
  /* --nix-surface and a lift, not a 5.5% grey: that literal was barely a step
     against the panel in both themes, so two of the three plan cards were
     text with no box around them, and it was the one raw colour in a
     component sheet. The surface alone is less of a step than the
     literal it replaced — so the lift is what actually draws the edge here,
     and it is already `none` in dark where the grounds do the work. */
  box-shadow: var(--lift-1);
  background: var(--nix-surface);
  border-radius: var(--radius-2xl);
  padding: 26px 24px;
  display: flex;
  flex-direction: column;
  gap: 13px;
}

.ab-price__card--lead {
  background: var(--nix-ink);
  color: var(--nix-on-ink);
}

.ab-price__name {
  font-size: clamp(15px, 1.15vw, 17px);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -0.01em;
  margin: 0;
}

.ab-price__fig {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  flex-wrap: wrap;
}

.ab-price__amount {
  font-size: 34px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
}

.ab-price__per {
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--nix-text-soft);
}

.ab-price__text {
  font-size: 13px;
  color: var(--nix-text-soft);
  line-height: 1.5;
  margin: 0;
}

.ab-price__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.ab-price__item {
  position: relative;
  padding-left: 18px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--nix-text-soft);
}

.ab-price__item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.52em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid currentColor;
}

/* Everything quiet inside the lead card, which is the one card whose ground
   is ink. .nix-plans__badge is on the list because it renders in here when the
   account is on the plan this card describes — soft on ink is under half the floor, and it
   was missing. */
.ab-price__card--lead .ab-price__per,
.ab-price__card--lead .ab-price__text,
.ab-price__card--lead .nix-plans__badge,
.ab-price__card--lead .ab-price__item {
  color: var(--nix-on-ink-soft);
}

.ab-price__cta,
.ab-price__cta-form {
  margin-top: auto;
  align-self: flex-start;
}

.ab-price__cta-form .ab-price__cta {
  margin-top: 0;
}

/* Bootstrap's own .btn loads after ours and washes the landing pills
   out to transparent; the landing look is re-asserted with the id in
   front so load order stops mattering. */
#wrapwrap .btn.ab-price__cta {
  display: inline-block;
  padding: 12px 28px;
  border: 0;
  border-radius: var(--radius-pill);
  font-size: 14px;
  letter-spacing: 0.02em;
  line-height: 1.5;
}

#wrapwrap .btn--paper.ab-price__cta {
  background: var(--nix-paper);
  color: var(--nix-ink);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
}

/* A STEP, not the ground it already has. Both of these restated their own
   background and colour and lifted a pixel, which is the defect .btn--ghost
   was fixed for one file away, on the same component name. */
#wrapwrap .btn--paper.ab-price__cta:hover {
  background: color-mix(in srgb, var(--nix-paper) 92%, var(--nix-text));
  transform: translateY(-1px);
}

/* Ink, not surface: on the dark card the surface pill vanished into its
   own ground. --nix-ink flips with the theme, so this is the white pill
   in dark mode and the dark one in light. */
#wrapwrap .btn--ghost.ab-price__cta {
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  box-shadow: var(--lift-control);
}

#wrapwrap .btn--ghost.ab-price__cta:hover {
  background: color-mix(in srgb, var(--nix-ink) 88%, var(--nix-paper));
  transform: translateY(-1px);
}

@media (max-width: 900px) {
  .ab-price {
    grid-template-columns: 1fr;
  }
}

.nix-plans__badge {
  font-size: 12px;
  color: var(--nix-text-soft);
}

.nix-plans__manage {
  margin-top: 24px;
}

.nix-plans__manage .btn {
  background: color-mix(in srgb, var(--nix-text) 7%, var(--nix-paper));
  color: var(--nix-text);
  border: none;
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  font-size: 13px;
}

/* ------------------------------------------------------------------ *
 * /my/devices — the account's installs. Same tones as the cards.
 * ------------------------------------------------------------------ */

/* ── THE SETTINGS HEADER ──────────────────────────────────────────────────
   Prepended to <main> on the four configuration pages (see the xpath in
   views/portal/layout.xml). Back on the left, the section's name in the
   middle.

   A THREE-COLUMN GRID, and the third column is empty on purpose. The title has
   to be centred on the PAGE; centred inside the space the Back link leaves
   would put it a link's width off centre, and the width of that link is the
   width of the word "Back" in whichever language is on. 1fr auto 1fr is the
   only arrangement where the middle cell's centre is the page's centre no
   matter what is beside it. */
#wrapwrap .nix-subhead {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 24px 0 0;
}

/* AND IN THE ACCOUNT IT STARTS AT THE PANEL'S OWN EDGE. The 24 above is for
   this bar drawn on a page that has padding of its own; here the panel already
   holds it off the top, and Settings — the page every one of these is one
   storey under — now opens with its title on that first line. A bar that
   started an inch lower made the two screens look like different rooms. */
#wrapwrap.nix-account > main.page > .nix-subhead {
  padding-top: 0;
  margin-bottom: 24px;
}

/* AND IT GIVES UP THE CORNER THE RING IS IN. With the rail sent away the menu
   ring is drawn over the panel's own top-left — 20px of circle centred about
   32px in — which is exactly where this bar's chevron was: two controls on the
   same pixels, and a press there was a coin toss between opening the menu and
   leaving the page. Reported as the back overlapping the circle.

   BOTH SIDES, not just the left. The title is centred by a 1fr auto 1fr grid,
   so padding on one side alone moves it half that far off the panel's centre —
   the one thing this bar exists to get right. 56 is the same number the three
   page bars deduct in components.css: the ring's 25 and 20, plus a thumb. */
body.is-shut:not(.is-nav) #wrapwrap.nix-account > main.page > .nix-subhead {
  padding-inline: 56px;
}

/* On a phone the rail covers the page rather than standing beside it, so the
   ring is over the panel whatever the rail's stored state says. */
@media (max-width: 720px) {
  #wrapwrap.nix-account > main.page > .nix-subhead {
    padding-inline: 56px;
  }
}

/* AND IT IS NOT THE PAGE BLOCK. This is the bug the first version shipped, and
   it is worth spelling out because nothing about the bar caused it.

   `#wrapwrap.nix-account > main.page > *:first-child` gives the page's own
   block `flex: 1 0 auto; min-height: 100%` — that is what keeps the site map
   footer below the fold on a two-line account page. Prepending this bar made
   the BAR the first child, so the bar became a full-panel-height box with its
   one row of content centred in it: measured on the deployed page, the row sat
   at y=670 with a screen of nothing above it and the form's own heading 790px
   below. Reported as "hay issues de css".

   Two rules, and both are needed: the bar takes its own height, and the block
   AFTER it takes the treatment that used to be :first-child's. The selectors
   carry :first-child so they outrank that rule rather than merely following
   it — same id, same two classes, and one more class than a bare `*`. */
#wrapwrap.nix-account > main.page > .nix-subhead:first-child {
  flex: none;
  min-height: 0;
}

#wrapwrap.nix-account > main.page > .nix-subhead:first-child + * {
  flex: 1 0 auto;
  min-height: 100%;
}

/* Quiet, because it is the way out of a page rather than a thing to do on it —
   the same weight the rail's own back row carries. */
#wrapwrap .nix-subhead__back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  justify-self: start;
  font-size: 14px;
  color: var(--nix-text-soft);
  text-decoration: none;
}

#wrapwrap .nix-subhead__back:hover {
  color: var(--nix-text);
  text-decoration: none;
}

#wrapwrap .nix-subhead__back svg {
  flex: none;
  width: 16px;
  height: 16px;
}

#wrapwrap .nix-subhead__back:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── AND WHAT IS UNDER THE BAR SITS UNDER IT ─────────────────────────────
   The title is centred on the panel and the page below it was not: Odoo lays
   its forms out as a Bootstrap col inside a full-width row, so Profile was a
   704px form pinned to the left edge with a heading centred over 946px of
   panel, and our own settings pages put their 42rem and 560px columns in the
   same corner for the same reason — a measure with no auto margin.

   Two rules, both hanging off the bar so nothing outside these pages moves:
   the blocks inside the page's own wrapper, and the blocks inside a Bootstrap
   container — which is where Odoo's form column is.

   NOT the rows. Centring those as well was the first attempt and it centred
   the wrong thing: Company Name and Country are half-width fields in rows of
   their own, so every short field in the form drifted to the middle of the
   column and the labels stopped sharing a left edge. The column moves; what is
   inside it stays where the form put it. */
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ * > * {
  margin-inline: auto;
}

#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ * .container > * {
  margin-inline: auto;
}

/* AND A LINE OF PROSE ON ONE OF THESE PAGES IS CENTRED WITH IT. The notices —
   "no tickets yet", "support is being set up", "ticket sent" — are full-width
   paragraphs with nothing to give them a measure, so an auto margin does
   nothing for them and they sat hard left under a centred title. They are the
   only thing on the page when they show. */
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ * > .nix-devices__empty,
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ * > .nix-devices__plan,
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ * .nix-pay__lede {
  text-align: center;
}

/* ── AND ODOO'S OWN TITLE IS THE SECOND ONE ──────────────────────────────
   Two of these pages are Odoo's markup and each opens with its own heading:
   /my/security an h3 reading "Connection & Security" and /my/payment_method a
   paragraph wearing .h4 reading "Payment method". Both say what the bar above
   them already says, one line lower and in a different voice — reported as
   "payment method tiene doble".

   Hidden rather than deleted: they are views we do not own, and an inherit
   that removes a node another module xpaths into is a registry that fails to
   load. Same reasoning as the native footer at the head of the layout.

   The rules that CENTRE these two stay where they are: they still apply on
   any page that draws them without the bar. */
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ .o_portal_wrap h3,
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ .wrap #o_payment_form_options > div:first-child > :first-child {
  display: none;
}

/* AND ODOO'S TWO FORMS SIT UNDER THAT BAR TOO. Both pages already draw their
   content at 44rem — the security page's sections and the payment page's card
   list and its button row are all capped there — inside a wrapper that spans
   the panel, so a 704px form stood in the corner of a 1560px box with a title
   centred over it.

   The cap moves up to the wrapper, which is what an auto margin needs to bite:
   the boxes inside are already this width, so nothing changes shape, the
   column just stops starting at the left edge. Written against the bar so the
   rules further up — which give both these wrappers `max-width: none` for the
   pages that draw them without one — are left alone. */
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ .wrap .o_payment_form,
#wrapwrap.nix-account > main.page > .nix-subhead:first-child ~ .o_portal_wrap .o_portal_security_body {
  max-width: 44rem;
  margin-inline: auto;
}


/* IT IS THE PAGE'S TITLE, so it is set like one — the same one. It was 14px
   and soft while it said "Settings", a section label above a page that titled
   itself; then 20px once it started naming the page. It is now exactly what
   .nix-login__title is on Settings itself, 26 at 300, because these pages are
   one storey under that one and a title that shrinks on the way in reads as a
   different kind of page rather than the next screen of the same one. */
#wrapwrap .nix-subhead__title {
  font-size: 26px;
  font-weight: 300;
  line-height: 1.2;
  color: var(--nix-text);
}

/* ── /my/settings — THE CONFIGURATIONS LIST ───────────────────────────────
   The app's System Settings screen has three blocks: the resource rings, the
   About table and the Configurations list. Only the third one transfers — the
   rings describe a machine and About describes an install, and this page has
   neither. What is left is a column of rows, which is what a settings index is
   when nothing is padding it out — not even the caption, which named one block
   among three over there and had nothing to separate this page from.

   The row IS .nix-devices__row's measures, deliberately: same padding, same
   radius, same lift, same 8 between. Two lists of rows on two pages of one
   account that agree on everything but the numbers is one product; two that
   each picked their own is two. It is a separate class rather than a reuse
   because a row here is a LINK and a device row is not, and the difference is
   in the states below, not in the box. */
#wrapwrap .nix-set {
  padding: 48px 0;
}

/* AND NONE OF THAT PADDING INSIDE THE ACCOUNT. The 48 above is for this list
   drawn on its own; in /my the panel already holds the page off its edges, so
   it was 48px of nothing between the panel's top and the title — every other
   account page starts at the panel's own padding and this one started an inch
   below them. Same line .nix-devices and .nix-dash take, for the same
   reason. */
#wrapwrap.nix-account .nix-set {
  padding: 0;
}

/* The title takes the account's own spacing under it, now that the caption
   that used to open the gap is gone. */
#wrapwrap.nix-account .nix-set .nix-login__title {
  margin: 0 0 24px;
  /* 300 and centred, which is what .nix-subhead__title is on every page this
     list leads to. It was 400 and hard left, so pressing a row swapped a
     bold left-hand title for a light centred one and the two screens read as
     two different products. The one rule that left-aligns account titles is
     deliberately light (two classes) so a per-page decision like this can
     outrank it — see the comment on it. */
  font-weight: 300;
  text-align: center;
}

#wrapwrap .nix-set__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
  margin-bottom: 8px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
  color: var(--nix-text);
  text-decoration: none;
}

/* A row that answers the pointer, because every one of them goes somewhere.
   The ground lifts rather than the text changing colour: the label is already
   the page's own text colour and a link that turns blue on hover would be the
   one place in this account that looks like a document. */
#wrapwrap .nix-set__row:hover {
  background: color-mix(in srgb, var(--nix-text) 6%, var(--nix-surface));
  color: var(--nix-text);
  text-decoration: none;
}

/* The chevron says "there is a page behind this", and it is quiet: it is the
   affordance, not the content. */
#wrapwrap .nix-set__row svg {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--nix-text-soft);
}

#wrapwrap .nix-set__row:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

#wrapwrap .nix-devices {
  padding: 48px 0;
}

#wrapwrap .nix-devices__plan {
  text-align: center;
  color: var(--nix-text-soft);
  margin: 0 0 32px;
}

#wrapwrap .nix-devices__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
  margin-bottom: 8px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

#wrapwrap .nix-devices__meta {
  font-size: 12px;
  color: var(--nix-text-soft);
}

#wrapwrap .nix-devices__revoked {
  color: var(--nix-text-soft);
}

#wrapwrap .nix-devices__empty {
  text-align: center;
  color: var(--nix-text-soft);
}

#wrapwrap .nix-devices__revoke {
  background: color-mix(in srgb, var(--nix-text) 7%, var(--nix-surface));
  color: var(--nix-text);
  border: none;
  border-radius: var(--radius-sm);
  padding: 8px 16px;
  font-size: 13px;
}

/* NOTHING THIS SHEET SERVES HAS A FIXED HEADER, so nothing clears one.
   The rule that used to stand here read `#wrapwrap:not(.nav-shell)`, which is
   exactly the SIGNED-IN shell — the one with a rail and no bar — and it spent
   105px of padding above every page of /my on a --head-h that nothing on this
   leg ever measures, because the script that measures it looks for a
   .site-head and this layout has none. It tied on specificity with the
   panel's own 12px and won on source order. */

/* ── /my/payouts — MONEY GOING THE OTHER WAY ──────────────────────────────
   A drawing of what paying an author looks like; there is no Connect account
   behind it yet (see agent_base/views/portal_templates.xml). What is worth
   keeping when the real thing lands is the ORDER of the type: the number the
   author gets is the loud one and the gross and the fee are the small print
   under it, because a row that leads with the price is a row about the buyer.

   Every measure here is 42rem, the same reading column /my/devices uses, so
   the two pages do not disagree about how wide a list is.
   ------------------------------------------------------------------ */

#wrapwrap.nix-account .nix-pay__lede {
  margin: 0 0 24px;
  color: var(--nix-text-soft);
}

/* THE PREVIEW BANNER. Quiet, but it does not hide: a figure on a payouts page
   that a reader takes for their own money and waits for is worse than no page
   at all. It goes when the numbers become real. */
#wrapwrap.nix-account .nix-pay__mock {
  max-width: 42rem;
  margin: 0 0 20px;
  padding: 10px 14px;
  border-radius: var(--radius-chip);
  background: var(--chip-tint);
  color: var(--nix-text-soft);
  font-size: 13px;
}

#wrapwrap.nix-account .nix-pay__card {
  max-width: 42rem;
  padding: 24px;
  border-radius: var(--radius-lg);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

#wrapwrap.nix-account .nix-pay__card-head {
  margin: 0 0 16px;
  font-size: 20px;
  font-weight: 400;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-pay__card-body {
  margin: 0 0 20px;
  color: var(--nix-text-soft);
  line-height: 1.5;
}

/* A LIST, NUMBERED, because it is a sequence and not three features. The
   count is drawn rather than left to the marker so it can sit in a fixed
   column and keep every line of prose starting on one x. */
#wrapwrap.nix-account .nix-pay__steps {
  margin: 0 0 24px;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

#wrapwrap.nix-account .nix-pay__step {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  color: var(--nix-text-soft);
  line-height: 1.5;
}

#wrapwrap.nix-account .nix-pay__step-n {
  flex: none;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--chip-tint);
  color: var(--nix-text);
  font-size: 13px;
}

#wrapwrap.nix-account .nix-pay__step strong {
  color: var(--nix-text);
}

/* The ink pill every other primary action on this site wears. !important on
   the colour for the reason .nix-support__new carries one: the shell paints
   every <a> in the panel from a later rule of equal weight, and without it
   the label on an inverted pill is invisible until hover. */
#wrapwrap.nix-account .nix-pay__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 22px;
  border-radius: var(--radius-pill);
  background: var(--nix-text);
  color: var(--nix-paper) !important;
  font-size: 15px;
  text-decoration: none;
  transition: opacity var(--dur-2) ease;
}

#wrapwrap.nix-account .nix-pay__cta:hover {
  opacity: 0.85;
  color: var(--nix-paper) !important;
}

#wrapwrap.nix-account .nix-pay__fine {
  margin: 16px 0 0;
  font-size: 13px;
  color: var(--nix-text-quiet);
  line-height: 1.5;
}

/* Verifying. The same chip a support ticket's stage wears, so a state on this
   page and a state on that one are one visual idea. */
#wrapwrap.nix-account .nix-pay__status {
  display: inline-block;
  margin: 0 0 12px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--chip-tint);
  color: var(--nix-text-quiet);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* THREE NUMBERS, AND THE MIDDLE ONE IS THE HONEST ONE: yours, waiting, gone.
   A single "balance" would hide the hold, and the hold is the thing an author
   writes in asking about. auto-fit so a phone stacks them without a second
   breakpoint of its own. */
#wrapwrap.nix-account .nix-pay__figs {
  max-width: 42rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

#wrapwrap.nix-account .nix-pay__fig {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px;
  border-radius: var(--radius-lg);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

#wrapwrap.nix-account .nix-pay__fig-n {
  font-size: 26px;
  font-weight: 300;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-pay__fig-l {
  font-size: 13px;
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-pay__next {
  max-width: 42rem;
  margin: 0 0 28px;
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-pay__next strong {
  color: var(--nix-text);
}

/* The same quiet section label the ticket list uses over its own rows. */
#wrapwrap.nix-account .nix-pay__sec {
  margin: 0 0 8px;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--nix-text-quiet);
}

#wrapwrap.nix-account .nix-pay__list {
  max-width: 42rem;
  margin-bottom: 28px;
}

#wrapwrap.nix-account .nix-pay__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
  margin-bottom: 8px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

#wrapwrap.nix-account .nix-pay__row-main {
  min-width: 0;
}

#wrapwrap.nix-account .nix-pay__row-meta {
  margin-top: 4px;
  font-size: 13px;
  color: var(--nix-text-quiet);
}

/* WHAT THE AUTHOR GETS IS THE BIG NUMBER. The price and the fee are under it
   at 13px; this is the figure the row exists to report. */
#wrapwrap.nix-account .nix-pay__row-amt {
  flex: none;
  font-size: 18px;
  color: var(--nix-text);
}

#wrapwrap.nix-account .nix-pay__done {
  flex: none;
  font-size: 13px;
  color: var(--nix-text-quiet);
}

#wrapwrap.nix-account .nix-pay__bank {
  max-width: 42rem;
  margin: 0;
  font-size: 13px;
  color: var(--nix-text-quiet);
}

/* ── /my/support — the ticket door ─────────────────────────────────────────
   Everything reads the shell's own tokens, so both themes come free: the
   primary is the app's inverse pill (text-colour ground, paper text), the
   secondary and the fields are text-colour tints. Colours are pinned on
   both states because the shell styles bare links. */
/* THE BUTTON IS IN THE BAR NOW, so it needs no header row to be positioned
   inside: it is the third cell of .nix-subhead's grid and sits at the end of
   it. The rules that made a block title span the panel with the button pulled
   out of flow on top of it went with the header they were about. */
#wrapwrap.nix-account .nix-subhead .nix-support__new {
  justify-self: end;
}
#wrapwrap.nix-account .nix-support__new,
#wrapwrap.nix-account .nix-support__submit {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 10px 18px; border: 0; border-radius: var(--radius-chip); cursor: pointer;
  background: var(--nix-text);
  /* !important, con dolor: el shell pinta <a> con su propio color en una
     regla posterior de igual especificidad — sin esto el texto del pill
     invertido desaparece en reposo y sólo se ve en hover. */
  color: var(--nix-paper) !important;
  font-size: 14px; font-weight: 400; line-height: 1; text-decoration: none;
  transition: opacity var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-support__new:hover,
#wrapwrap.nix-account .nix-support__submit:hover { opacity: 0.85; color: var(--nix-paper) !important; }
#wrapwrap.nix-account .nix-support__cancel {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 10px 18px; border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--nix-text) 8%, transparent);
  color: var(--nix-text) !important;
  font-size: 14px; line-height: 1; text-decoration: none;
  transition: background var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-support__cancel:hover { background: color-mix(in srgb, var(--nix-text) 14%, transparent); color: var(--nix-text) !important; }
#wrapwrap.nix-account .nix-support__actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
#wrapwrap.nix-account .nix-support__field {
  width: 100%; padding: 14px 16px; border: 0; outline: none;
  border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--nix-text) 6%, transparent);
  color: var(--nix-text); font: inherit;
}
#wrapwrap.nix-account .nix-support__field::placeholder { color: var(--nix-text-soft); }
#wrapwrap.nix-account .nix-support__field:focus { background: color-mix(in srgb, var(--nix-text) 10%, transparent); }
/* A ring, not just a ground. These four fields carry `outline: none` and a
   background change, and a background change is not a focus state: it is the
   same cue hover uses and it is invisible to anyone who cannot see the two
   side by side. */
#wrapwrap.nix-account .nix-support__field:focus-visible,
#wrapwrap.nix-account .nix-ticket__input:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}
/* ── /my/developer — the form somebody becomes a publisher on ─────────────
   Its own block rather than more .nix-pay__ classes: that family is a page of
   READ-ONLY figures with one link at the bottom, and this is fourteen inputs.
   Reusing it shipped labels and fields running inline down the page like a
   plain-HTML form from 1998, because none of those classes has ever had to
   lay out a label above a control.

   The shape is the support form's, which is the only other real form in the
   portal: one column, label over control, the same tinted fill and the same
   focus ring. A second look for the same job is a second thing to keep. */
/* CENTRED AND UNBOXED, the way the app's own wizard asks things.
   A card would be right if this sat beside other content; it does not — it is
   the whole page, one question at a time, and a panel drawn around a single
   column just tells the reader there is something else on the page when there
   is nothing else on the page. Same reason the app's create wizard has no card
   either. */
.nix-dev__form {
  display: flex;
  flex-direction: column;
  gap: 22px;
  width: 100%;
  max-width: 30rem;
  margin: 32px auto 0;
  text-align: center;
}

/* Steps first, then the buttons, then the progress marks — the marks sit at
   the FOOT of the column, where the app puts them, rather than above a heading
   they would otherwise be read as belonging to. Ordered in CSS so the markup
   can stay in the order a no-JavaScript reader needs: every step, then the
   submit that works without any of this. */
.nix-dev__form > .nix-dev__step { order: 1; }
.nix-dev__form > .nix-dev__nav { order: 2; }
.nix-dev__form > .nix-dev__dots { order: 3; }

/* The step's own name, and the only heading on the page besides the title. */
#wrapwrap.nix-account .nix-dev__head {
  margin: 0;
  font-size: 22px;
  font-weight: 400;
  color: var(--nix-text);
}

/* THE COLUMN IS CENTRED; WHAT IS INSIDE A FIELD IS NOT. A label sits on the
   left edge of the control it names, because that is the edge the eye is
   already on when it moves down into the box — a centred label leaves the
   reader jumping left, right, left down the form. The step's heading and the
   buttons stay centred with the column; only the fields read as a form. */
.nix-dev__field { display: flex; flex-direction: column; gap: 6px; text-align: left; }

#wrapwrap.nix-account .nix-dev__field > span {
  font-size: 13px;
  color: var(--nix-text-soft);
}

#wrapwrap.nix-account .nix-dev__field input,
#wrapwrap.nix-account .nix-dev__field select,
#wrapwrap.nix-account .nix-dev__field textarea {
  width: 100%;
  padding: 12px 14px;
  border: 0;
  outline: none;
  border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--nix-text) 6%, transparent);
  color: var(--nix-text);
  font: inherit;
  /* The label above is centred with the column; the VALUE is not. Text a
     person types reads left-to-right from a fixed edge, and centring it makes
     every keystroke move the whole string. */
  text-align: left;
}

#wrapwrap.nix-account .nix-dev__field textarea { resize: vertical; min-height: 96px; }

#wrapwrap.nix-account .nix-dev__field input:focus,
#wrapwrap.nix-account .nix-dev__field select:focus,
#wrapwrap.nix-account .nix-dev__field textarea:focus {
  background: color-mix(in srgb, var(--nix-text) 10%, transparent);
}

/* A ring, not just a ground — same reasoning as the support fields above:
   a background change is the cue hover already uses. */
#wrapwrap.nix-account .nix-dev__field input:focus-visible,
#wrapwrap.nix-account .nix-dev__field select:focus-visible,
#wrapwrap.nix-account .nix-dev__field textarea:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* A fact rather than a control — the handle, once it can no longer change. */
#wrapwrap.nix-account .nix-dev__note {
  margin: 0;
  color: var(--nix-text-soft);
  font-size: 14px;
  line-height: 1.5;
  text-align: left;
}
#wrapwrap.nix-account .nix-dev__note strong { color: var(--nix-text); }

#wrapwrap.nix-account .nix-dev__field small {
  font-size: 12px;
  color: var(--nix-text-quiet);
  line-height: 1.45;
}

/* The agreement: a checkbox and a sentence on one line, which is the one
   place in this form where the control belongs beside the words. */
.nix-dev__check { flex-direction: row; align-items: center; justify-content: center; gap: 10px; }
#wrapwrap.nix-account .nix-dev__check input { width: auto; padding: 0; margin-top: 3px; }
#wrapwrap.nix-account .nix-dev__check > span { color: var(--nix-text); font-size: 15px; }

#wrapwrap.nix-account .nix-dev__submit {
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 22px;
  border: 0;
  cursor: pointer;
  border-radius: var(--radius-pill);
  background: var(--nix-text);
  color: var(--nix-paper) !important;
  font: inherit;
  font-size: 15px;
  transition: opacity var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-dev__submit:hover { opacity: 0.85; }

/* ── the four steps ───────────────────────────────────────────────────────
   A fieldset carries no look of its own here: the block IS the step, and the
   browser's default border and padding would draw a box around each one and
   turn a sequence into four cards. */
.nix-dev__step {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.nix-dev__step[hidden] { display: none; }

/* Progress, drawn rather than counted out in words: four marks, filled up to
   where the reader is. Not links — a step you have not reached has nothing
   to show, and a step behind you is one Back away. */
.nix-dev__dots { display: flex; justify-content: center; gap: 8px; margin: 8px 0 0; padding: 0; list-style: none; }
#wrapwrap.nix-account .nix-dev__dot {
  width: 34px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--nix-text) 12%, transparent);
  transition: background var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-dev__dot.is-on { background: var(--nix-text); }

.nix-dev__nav { display: flex; align-items: center; justify-content: center; gap: 10px; }
.nix-dev__nav[hidden] { display: none; }
#wrapwrap.nix-account .nix-dev__back {
  display: inline-flex;
  align-items: center;
  padding: 12px 20px;
  border: 0;
  cursor: pointer;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--nix-text) 8%, transparent);
  color: var(--nix-text);
  font: inherit;
  font-size: 15px;
  transition: background var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-dev__back:hover { background: color-mix(in srgb, var(--nix-text) 14%, transparent); }
#wrapwrap.nix-account .nix-dev__back[hidden] { display: none; }

/* ── /my/marketplace — a list of places, same row the support list uses ──── */
.nix-dev__rows { display: flex; flex-direction: column; gap: 8px; max-width: 42rem; }
#wrapwrap.nix-account .nix-dev__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 18px;
  border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--nix-text) 5%, transparent);
  color: var(--nix-text) !important;
  text-decoration: none;
  transition: background var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-dev__row:hover { background: color-mix(in srgb, var(--nix-text) 9%, transparent); }
.nix-dev__row-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
#wrapwrap.nix-account .nix-dev__row-meta { color: var(--nix-text-soft); font-size: 13px; }
#wrapwrap.nix-account .nix-dev__row-go { width: 20px; height: 20px; flex: none; color: var(--nix-text-quiet); }

.nix-support__form { display: flex; flex-direction: column; gap: 12px; max-width: 560px; margin-top: 8px; }
.nix-support__body { resize: vertical; min-height: 120px; }
.nix-support__list { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; max-width: 560px; }
#wrapwrap.nix-account .nix-support__row {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  padding: 12px 16px; border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--nix-text) 5%, transparent);
  color: var(--nix-text) !important; text-decoration: none;
}
#wrapwrap.nix-account .nix-support__row:hover { background: color-mix(in srgb, var(--nix-text) 9%, transparent); color: var(--nix-text) !important; }
.nix-support__stage { color: var(--nix-text-quiet); font-size: 13px; white-space: nowrap; }
/* The ticket page is a chat session: bar on top, bubbles, composer under. */
#wrapwrap.nix-account .nix-ticket { display: flex; flex-direction: column; min-height: calc(100svh - 96px); }
.nix-ticket__bar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 18px; }
#wrapwrap.nix-account .nix-ticket__back {
  justify-self: start; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 10px;
  color: var(--nix-text) !important;
  background: transparent;
}
#wrapwrap.nix-account .nix-ticket__back:hover { background: color-mix(in srgb, var(--nix-text) 12%, transparent); }
.nix-ticket__word { font-size: 17px; }
.nix-ticket__end { justify-self: end; display: inline-flex; align-items: center; gap: 12px; }
#wrapwrap.nix-account .nix-ticket__infobtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: var(--radius-pill);
  font-family: Georgia, serif; font-style: italic; font-size: 15px;
  color: var(--nix-text) !important;
  background: color-mix(in srgb, var(--nix-text) 8%, transparent);
}
#wrapwrap.nix-account .nix-ticket__infobtn:hover,
#wrapwrap.nix-account .nix-ticket__infobtn.is-on { background: color-mix(in srgb, var(--nix-text) 16%, transparent); }
.nix-ticket__thread { display: flex; flex-direction: column; gap: 10px; flex: 1; }
#wrapwrap.nix-account .nix-ticket__bubble {
  max-width: 62%; padding: 12px 16px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--nix-text) 6%, transparent);
  align-self: flex-start;
}
#wrapwrap.nix-account .nix-ticket__bubble.is-mine {
  align-self: flex-end;
  background: color-mix(in srgb, var(--nix-text) 12%, transparent);
}
.nix-ticket__bubblebody p { margin: 0 0 6px; }
.nix-ticket__bubblebody p:last-child { margin: 0; }
.nix-ticket__bubblemeta { margin-top: 6px; font-size: 12px; color: var(--nix-text-quiet); }
/* ONE pill: + | text | round send — the app's composer shape. */
#wrapwrap.nix-account .nix-ticket__composer {
  display: flex; align-items: center; gap: 8px; margin-top: 18px;
  width: min(100%, 720px); margin-left: auto; margin-right: auto;
  padding: 6px 6px 6px 16px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--nix-text) 6%, transparent);
}
#wrapwrap.nix-account .nix-ticket__attach {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none; cursor: pointer;
  color: var(--nix-text-soft); border-radius: var(--radius-pill);
}
#wrapwrap.nix-account .nix-ticket__attach:hover { color: var(--nix-text); }
/* NOT display:none. That takes the input out of the tab order, and the
   thing wrapping it is a <label>, which is not focusable — so a support
   ticket could only be given a file with a pointer. Off screen and still in
   the order; the label lights up with it through :focus-within. */
.nix-ticket__fileinput {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

#wrapwrap.nix-account .nix-ticket__attach:focus-within {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}
#wrapwrap.nix-account .nix-ticket__input {
  flex: 1; padding: 10px 4px; border: 0; outline: none;
  background: transparent; color: var(--nix-text); font: inherit;
}
#wrapwrap.nix-account .nix-ticket__input::placeholder { color: var(--nix-text-soft); }
#wrapwrap.nix-account .nix-ticket__send {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none; border: 0; border-radius: var(--radius-pill); cursor: pointer;
  background: var(--nix-text); color: var(--nix-paper) !important;
  transition: opacity var(--dur-2) ease;
}
#wrapwrap.nix-account .nix-ticket__send:hover { opacity: 0.85; }
.nix-ticket__files { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
#wrapwrap.nix-account .nix-ticket__file {
  display: inline-flex; padding: 6px 12px; border-radius: var(--radius-pill); font-size: 12px;
  background: color-mix(in srgb, var(--nix-text) 10%, transparent);
  color: var(--nix-text) !important; text-decoration: none;
}
#wrapwrap.nix-account .nix-ticket__file:hover { background: color-mix(in srgb, var(--nix-text) 16%, transparent); }
.nix-ticket__pending { display: flex; flex-wrap: wrap; gap: 8px; width: min(100%, 720px); margin: 0 auto 10px; }
#wrapwrap.nix-account .nix-ticket__filex {
  border: 0; background: transparent; cursor: pointer; padding: 0 0 0 6px;
  color: inherit; font-size: 14px; line-height: 1;
}
.nix-ticket__about { flex: 1; display: flex; align-items: flex-start; justify-content: center; padding-top: 6svh; }
#wrapwrap.nix-account .nix-ticket__aboutcard {
  width: min(92%, 520px); display: flex; flex-direction: column; gap: 14px;
  padding: 22px 24px; border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--nix-text) 5%, transparent);
}
.nix-ticket__aboutrow { display: flex; flex-direction: column; gap: 3px; font-size: 14px; }
.nix-ticket__aboutlabel { font-size: 12px; color: var(--nix-text-quiet); letter-spacing: 0.04em; text-transform: uppercase; }
/* The thread itself is .nix-ticket__* above — the bubbles, their author line
   and their stage. A .nix-support__thread with .nix-support__msg children was
   the first version of the same screen, and it went on being styled here for
   as long as nothing looked. */


/* ── Dark ─────────────────────────────────────────────────────────────────
   The sign-in card and the notification panel spend --lift-*, and those are
   already `none` on this ground, so nothing has to be said about them: a
   shadow is light being blocked and on #101010 what the browser draws instead
   is a black smear. One surface is not answered by the token, and here is why.

   A POPOVER IS NOT A CARD. A card can lose its shadow in dark because it sits
   ON a ground and the step between the two is its edge; this floats over
   whatever the page happens to be showing, and a menu with no edge over a
   panel a few per cent away from it is a menu with no edge. A hairline
   instead of a smear. */
/* Focus on the controls this leg still owns. The rail's own seats are
   .nav-rail__item and shell.css rings them; what is left here is the avatar
   menu's rows and the two fields that are this sheet's. */
#wrapwrap .nix-popover__item:focus-visible,
#wrapwrap.nix-account .nix-store__search input:focus-visible,
#wrapwrap .nix-login__signup:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── Overrides that have to stand last ────────────────────────────────────
   A media query adds no specificity, so a block written above a rule of the
   same selector loses to it at every width. These were higher up and every
   declaration in them was dead — including the reduced-motion opt-out, which
   is not a nicety: it is what a reader who asked their operating system for
   no motion gets. Position is the only thing that makes them apply. */

@media (max-width: 720px) {
  #wrapwrap > main.page > .oe_website_login_container {
    padding: 24px;
  }
}

@media (max-width: 720px) {
  /* AND THE HEADING CENTRES AT EVERY STATE HERE, not only with the rail sent
     away. On a desktop the ring is on the page for one state out of three, so
     the heading only has to move out of its way in that one. At this width
     the rail covers rather than stands beside, which means the ring is the
     way in and is therefore in the panel's top-left corner ALWAYS — and the
     account pages open with an h1 at exactly that corner. "Devices" was
     drawn with a circle through its first letter in every state a phone has.

     Same three kinds of title and same three subtitle lines as the desktop
     rule, for the same reasons — see the comment on it. Two rules rather than
     one because the states differ, not because the headings do; if one of
     these lists grows the other one has to grow with it. */
  #wrapwrap.nix-account > main.page h1,
  #wrapwrap.nix-account .o_portal_wrap h3,
  #wrapwrap.nix-account .wrap #o_payment_form_options > div:first-child > :first-child,
  #wrapwrap.nix-account .nix-devices__plan,
  #wrapwrap.nix-account .nix-plans__current,
  #wrapwrap.nix-account .nix-pay__lede,
  #wrapwrap.nix-account .nix-store__lede {
    text-align: center;
  }
}


/* ── The quotation has no side column ───────────────────────────────────────
   Odoo lays the sale order portal out as a row: a 3-4 column sidebar carrying
   the total, Accept & Sign, View Details, the anchor list, the salesperson and
   the EDI button, and beside it the document itself. On this site that column
   was a tall strip of controls against a mostly empty panel, with the number
   the reader came for parked in a corner instead of over the thing it prices.

   So the row becomes a stack and the sidebar rides ABOVE the document, centred
   over it: total, then the buttons, then who to talk to, then the document
   starting at "Quotation - S00011". Done in CSS and not by overriding
   sale.sale_order_portal_template, because nothing about the markup is wrong —
   it is two blocks in the wrong order for this panel, and an override would
   pin us to Odoo's arch across every upgrade for a layout change.

   The anchor list goes: it is a table of contents for two headings that are
   now both above the fold, and stacked under the total it read as two stray
   links. */
.o_portal_sale_sidebar,
.o_portal_invoice_sidebar {
  flex-direction: column;
}

.o_portal_sale_sidebar > .col-lg-4,
.o_portal_invoice_sidebar > .col-lg-4 {
  width: 100%;
  max-width: none;
  flex: 0 0 auto;
}

/* The rule between the two columns has no two columns to divide. Stacked it
   did not go away, it stood on end down the right-hand edge of the panel for
   the height of the block — .d-lg-block is a Bootstrap display utility and
   those carry !important, so a specificity win is not enough here. Same for
   the two below.

   Powered by Odoo goes with it, and so does Connect with your software!: the
   EDI button opens a modal explaining how to import this document into Odoo,
   which is addressed to somebody running Odoo — not to the person being asked
   to sign a quotation. Both were furniture between the total and the document. */
.o_portal_sale_sidebar > .col-lg-4 > .vr,
.o_portal_invoice_sidebar > .col-lg-4 > .vr,
#sidebar_content > .d-none.text-center.text-muted,
#portal_connect_software_modal_btn {
  display: none !important;
}

/* !important, and it took a card behind it to make the loss visible. pe-lg-4 is
   Bootstrap's padding-end utility and every utility ships !important, so this
   reset — one id against one class — had never once applied. It cost nothing
   while the sidebar was a column on a panel; inside the dock it was 24px of
   empty surface down the right-hand side, and the card looked padded on one
   side only. Measured: computed padding-right was 24px with the reset in the
   sheet. */
#sidebar_content {
  padding-right: 0 !important;
  /* AND THE MARGIN NEEDS THE SAME WEIGHT AS THE PADDING DID. mb-4 mb-lg-0: the
     zero only applies from 992px up, so below it Bootstrap put 24px under this
     block and this reset — one id against one class — lost to it exactly as the
     padding had. On a desktop it never showed; on a phone it was the band of
     empty surface under the buttons.

     Proved rather than assumed: with mb-lg-0 taken off the element, the dock
     measured 88px and #sidebar_content's computed margin-bottom read 24px. With
     the !important added, 64px and 0px in both states. */
  margin-bottom: 0 !important;
}

/* THE TOTAL GOES ALTOGETHER. Stacked across the panel it was a number the
   width of a headline sitting above the document that itemises it — and the
   document ends in a totals block that says the same figure, in the row
   labelled Total, next to the two lines that make it up. First thing on the
   page was the answer, out of reach of anything explaining it, and at the far
   left where the rail's ring overlapped its currency sign.

   The whole wrapper and not just the h2: it is a flex row that exists to hold
   that heading, and an empty one still takes its gap. */
#sidebar_content > .position-relative {
  display: none !important;
}

/* ── The box between the card and the buttons owns nothing ────────────────
   Odoo's entries wrapper — .d-flex.flex-column.gap-4.mt-3 — is the one element
   between #sidebar_content and the row of controls, and it was quietly
   deciding two things it had no business deciding.

   align-items: center made the row shrink to its content, which is why three
   buttons given an equal share of the width still came out the width of their
   own labels: the share being divided was the labels' own. Fixed on the row
   with align-self, and kept here because the wrapper's other children — before
   they were all hidden — did want centring.

   AND mt-3 IS SIXTEEN PIXELS INSIDE A CARD. It is Bootstrap's margin for a
   block starting under a heading, and the heading is hidden; what was left was
   a band of empty surface across the top of the dock, which is what made the
   card look padded on one side only. Margin utilities are !important, so this
   is. */
#sidebar_content > .d-flex.flex-column {
  align-items: center;
  width: 100%;
  margin: 0 !important;
  /* AND gap-4 SPACES A GHOST. Measured: this wrapper has four children and
     three of them are display:none — but the fourth is the empty <div> that
     held the salesperson block, still a flex item at zero height. gap-4 puts
     24px between the button row and that nothing, which is the band of empty
     surface that appeared along the bottom of the dock the moment the row got
     a card of its own. Dock height 88 with the gap, 64 without: ten of padding,
     forty-four of buttons, ten of padding. */
  gap: 0 !important;
}

/* Stacked buttons are a column because the sidebar was one; across the page
   they sit side by side. */
/* !important, for the reason it always is here: .flex-column is a Bootstrap
   utility on this element and utilities are !important, so a plain
   flex-direction never reached it and the buttons stayed stacked. */
#sale_order_sidebar_button,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) {
  /* THREE EQUAL BUTTONS, and the width is shared rather than measured. They
     come from three modules with three labels — Download, Pay Now / Sign & Pay
     / Accept & Sign, Feedback — and the middle one changes wording with the
     state of the order, so laying them out by their content means a row that
     shifts under the reader between one visit and the next. flex-basis 0 makes
     the text irrelevant: the row is divided in three and the words sit inside
     whatever they get.

     And the row is capped. Across a 880px document three equal thirds are three
     banners; 480 is where a button still reads as a button. */
  /* AND IT TAKES THE WIDTH BEFORE IT CAPS IT, which is the line that was
     missing and the reason three equal buttons were not equal. Traced in the
     served HTML rather than guessed at: the row's parent is
     #sidebar_content > .d-flex.flex-column, and the rule twenty lines above
     gives that parent align-items: center. A centred flex item shrinks to its
     content, so the row was never 480 wide — it was as wide as three labels,
     and flex-basis 0 divided that between them. Every button therefore came out
     the width of its own word, which is exactly what the basis was set to
     prevent.

     align-self overrides the parent's align-items for this one child; width
     100% is what it then fills, and max-width is what stops it. */
  /* The card above is what is capped and centred now; the row fills it. */
  align-self: stretch;
  width: 100%;
  flex-direction: row !important;
  flex-wrap: wrap;
  /* stretch and not center: the three come from three different places in
     Odoo's markup with three different paddings, and centred they sat at
     three different heights. */
  align-items: stretch;
  justify-content: center;
}

/* DOWNLOAD, THE ACTION, FEEDBACK. The one thing to do is in the middle, with a
   way to keep the document on one side and a way to ask about it on the other.
   Ordered by name rather than by moving markup: the action itself is a
   different element in each state — Accept & Sign, Sign & Pay, Pay Now — and
   the only one of the three that is always the same node is the one that is
   NOT the action. So everything defaults to the middle and the two that are
   named step out of it. */
#sale_order_sidebar_button > *,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > * { order: 2; }
#sale_order_sidebar_button > .o_download_pdf,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > .o_download_pdf { order: 1; }
#sale_order_sidebar_button > .nix-chat__feedback,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > .nix-chat__feedback { order: 3; }

/* One height, written down. Stretching them made the boxes equal and left
   the labels on different lines — the three buttons come from three places in
   Odoo's markup with three different paddings, so Pay Now measured 44 against
   38 for the other two. */
/* AND NO MARGIN OF ITS OWN. #wrapwrap .btn-primary carries margin-top: 12px
   for the buttons it was written for — the sign-in form's, stacked — and in a
   row that margin is the only reason the middle button sat 12px below the
   other two. Measured: y 224, 236, 224. */
/* ONLY THE ACTION IS WHITE. Download and Feedback are .btn-light, and this
   stylesheet only ever skinned .btn-light under #wrapwrap.nix-account — the
   signed-in account. A quotation opened from an email carries no such class,
   so both fell back to Bootstrap's own light: measured rgb(246, 245, 244) on
   rgb(33, 37, 41), two white buttons either side of the white one that is
   supposed to be the only one.

   Scoped to this group rather than lifted off .nix-account, because that class
   is what tells the rest of the sheet it is styling an account page and this
   page is not one. */
/* OPAQUE, and the second colour in the mix is the whole point. These were 8%
   ink over TRANSPARENT, which is fine for a button sitting still on a panel and
   wrong for one the document scrolls behind: with the bar's own background
   taken away, line items and phase names climbed straight through the words.
   Mixed into --nix-panel instead of into nothing, the fill computes to exactly
   the same colour it was and stops being a window. */
/* A FLAT COLOUR, WRITTEN DOWN, ONE PER THEME. These were a wash of the page's
   ink over the page's ground, which is the right way to tint something sitting
   still on a panel and the wrong way to fill a control that floats over a
   scrolling document: however opaque the result computed to be, it was derived
   from whatever was behind it and it read as a shape you could see through. Two
   hex values instead, each chosen against its own theme's ground, and nothing
   left to reason about. */
#sale_order_sidebar_button .btn-light,
#sale_order_sidebar_button .btn-secondary,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn-light,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn-secondary {
  background: #E8E7E5;
  color: #1A1A1A;
  border: 0;
}

html[data-theme="dark"] #sale_order_sidebar_button .btn-light,
html[data-theme="dark"] #sale_order_sidebar_button .btn-secondary,
html[data-theme="dark"] #sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn-light,
html[data-theme="dark"] #sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn-secondary {
  background: #2E2D2B;
  color: #F4F3F1;
}

/* ── flex-basis 0 DOES NOT MEAN "no width", it means "no CONTENT width" ────
   Measured in the page rather than reasoned about, and the numbers are the
   whole explanation: 161.3 / 129.3 / 161.3 inside a 452px row. All three items
   reported an identical `flex: 1 1 0px`, min-width 0, no max-width — and came
   out unequal anyway.

   The difference is 32px, which is exactly the 16px of horizontal padding the
   two anchors carry and the Download wrapper does not. A flex base size of zero
   is the base size of the CONTENT box; padding is added on top of it before the
   free space is shared. So the row had 388px to divide rather than 452, gave
   129.3 to each, and then handed the two padded items their 32px back.
   border-box does not save you here: it governs the `width` property, not the
   flex base size.

   A percentage basis does, because a percentage basis on a border-box element
   IS the border-box size. One third of the row minus its two gaps, and the
   three finish at 158.7 each — measured after the change, in the same page.

   grow and shrink stay at 1 so the row still works when there are two buttons
   rather than three: Pay Now is not drawn once the order is paid, and two items
   asking for a third each simply grow to a half each. */
#sale_order_sidebar_button > *,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > * {
  flex: 1 1 calc((100% - 16px) / 3);
  min-width: 0;
}

/* AND THE ONE THAT IS WRAPPED FILLS ITS SHARE. Download is not a button in this
   row, it is a <div> holding a button — Odoo wraps it in .o_download_pdf, its
   own flex box, so the third the row hands out goes to the wrapper and the
   anchor inside it shrinks to the width of the word. Measured against the other
   two it came out visibly narrower, which is the one thing three equal buttons
   must not be. The wrapper passes the width through. */
#sale_order_sidebar_button > *:not(.btn),
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > *:not(.btn) {
  display: flex;
}

#sale_order_sidebar_button > *:not(.btn) > .btn,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > *:not(.btn) > .btn {
  flex: 1 1 auto;
  width: 100%;
}

#sale_order_sidebar_button > *,
#sale_order_sidebar_button .btn,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > *,
#sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn {
  margin: 0 !important;
  min-height: 44px;
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

#sidebar_content .navspy {
  display: none;
}

#quote_content,
#invoice_content {
  width: 100%;
  max-width: none;
  flex: 0 0 auto;
  margin-top: 2rem;
}

/* THE LINE TABLE KEEPS WHAT IS BEING BOUGHT, NOT HOW IT ADDS UP. Taxes and
   Amount are both restated under the table — "Tax 15%" and "Total" are in the
   totals block a few rows down — so across the full width of the panel the
   table was spending two of its five columns saying twice what the summary
   says once.

   Matched on the td's own name attribute, not on nth-child: the discount
   column is conditional (display_discount), so a positional selector points at
   Taxes on one quotation and at Amount on the next. The section and combo
   subtotal cells go with them or a grouped quotation would keep a column the
   header no longer has. !important because the taxes cells carry
   d-md-table-cell, a Bootstrap display utility. */
#sales_order_table #taxes_header,
#sales_order_table #subtotal_header,
#sales_order_table td[name="td_product_taxes"],
#sales_order_table td[name="td_product_subtotal"],
#sales_order_table td[name="td_combo_taxes"],
#sales_order_table td[name="td_combo_subtotal"],
#sales_order_table td[name="td_section_price"] {
  display: none !important;
}

/* AND THE QUANTITY IS A NUMBER, NOT A NUMBER AND A NOUN. Odoo prints the unit
   of measure beside it — "1.00 Units" — which is the warehouse's word, not the
   customer's, and on a services quotation it says nothing at all. The figure
   stays; the unit goes.

   The second span inside the cell's div, because neither span carries a class
   or an id of its own: the div does (#quote_qty, repeated on every line), and
   the two spans are the quantity and the unit in that order. A quotation whose
   lines print no unit has one span and this matches nothing, which is the
   right answer there too. */
#sales_order_table td[name="td_product_quantity"] > div > span:nth-child(2),
#sales_order_table td[name="td_combo_quantity"] > div > span:nth-child(2) {
  display: none !important;
}

/* The heading views/portal/sale_order.xml puts above the page, and the copy
   Odoo prints inside the document going quiet. Not deleted there: it belongs
   to sale.sale_order_portal_content, which QWeb cannot move a node out of —
   see the head of that file. */
.nix-quote-title {
  text-align: center;
  margin-bottom: 24px;
}

/* The record's number on its own line under the word for what it is, and NOT
   in italics: <em> is Odoo's markup for it, and the emphasis was doing nothing
   here — the number is the biggest thing on the page already, and a slanted
   S00011 read as a different typeface rather than as a stress. */
.nix-quote-title h2 em {
  display: block;
  font-style: normal;
}


/* Sale Information, which is now two rows printed above. Its heading was the
   only one of the document's four that named the page rather than a party to
   it. */
#quote_content #sale_info {
  display: none;
}

#quote_content #introduction {
  display: none;
}

/* ── Communication history is the third panel ───────────────────────────────
   It was the last and the longest block on the quotation — a composer, a send
   button and every notification the order has produced — below the signature
   and the totals. It is now a panel on the right, opened by the chat button in
   the corner, and it behaves the way the rail on the other side behaves: it
   does not lie ON the page, it takes width FROM it. An overlay would cover the
   totals and the signature at the moment somebody opens the thread to ask
   about them.

   NOT A THIRD GRID COLUMN, and the reason is two paragraphs down: this block
   is nowhere near being a child of .nav-shell, and it must not be moved there.
   The page gives up the width with a margin instead, on the same curve and the
   same duration the shell animates its own columns with, and the panel takes
   what was given up. The two are separate rules that have to agree on one
   number, which is why the number is a variable and is written once.

   THE THREAD IS NEVER MOVED. portal.message_thread mounts an OWL app into
   #chatterRoot, and re-parenting a mounted app's host is a class of bug that
   does not announce itself — see the head of static/src/portal/quote_chat.js.
   The block stays exactly where Odoo rendered it, three levels inside
   #quote_content, and everything here is done from where it stands. */
#wrapwrap {
  --nix-chat-w: 380px;
  /* THE SAME SEAM THE RAIL HAS, and it took three values to land back on it.
     24 read as a corridor between two panels; 16 still read as a gap somebody
     had chosen. It is one gutter down the whole shell — rail to page, page to
     thread — and the answer was the number already written for the other
     side. */
  --nix-chat-seam: var(--shell-gap);
}

.nix-chat__trigger {
  position: fixed;
  /* The panel's own corner, not a corner of the window: --shell-gap is where
     the panel starts and 8 more is the inset inside it. It was 16 and sat a
     line below the top of the sheet, which reads as floating rather than
     pinned. */
  top: calc(var(--shell-gap) + 8px);
  right: calc(var(--shell-gap) + 16px);
  z-index: 21;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  /* NO DISC BEHIND IT. The surface was there to lift the control off the
     panel, and on a panel this quiet it read as a button that had been left
     pressed. The glyph is the control. */
  background: transparent;
  color: var(--nix-text);
  cursor: pointer;
  transition: right 420ms var(--ease-out-expo);
}

/* It travels with the edge it is pinned to rather than being hidden: a control
   that disappears when it is pressed leaves nothing saying what just happened
   or how to undo it. */
html.nix-chat-open .nix-chat__trigger {
  right: calc(var(--shell-gap) + var(--nix-chat-w) + var(--nix-chat-seam) + 16px);
}

.nix-chat__trigger svg {
  width: 18px;
  height: 18px;
}

/* THE PAGE GIVES UP THE WIDTH. Same duration and same curve as .nav-shell's
   own grid transition, so the panel and the rail move like two halves of one
   thing rather than two animations that happen to overlap. */
#wrapwrap.nav-shell main.page {
  transition: margin-right 420ms var(--ease-out-expo);
}

html.nix-chat-open #wrapwrap.nav-shell main.page {
  margin-right: calc(var(--nix-chat-w) + var(--nix-chat-seam));
}

/* The rule that divided the totals from the thread has nothing left to
   divide. :has, so it is the hr in front of THIS block and not every hr in
   the document. */
#quote_content hr:has(+ .nix-thread) {
  display: none;
}

/* The same surface, radius and inset the page panel has, on the other side of
   the same gutter — see .page in shell.css, which is where both numbers come
   from.

   visibility, and not only the transform: off-screen is not closed. A
   translated panel is still in the tab order and still read aloud, so a reader
   on a keyboard tabs out of Accept & Sign into a composer they cannot see. It
   is the one property that takes both away and still lets the slide animate —
   delayed to the end of the transition on the way out, immediate on the way
   in. */
/* AND IT STARTS AT THE TOP OF THE PANEL. #invoice_communication carries mt-4
   in Odoo's markup — a margin written for a block at the foot of a document,
   which on a fixed panel is 24px added under the `top` it was given, so the
   invoice's thread hung a line lower than the quotation's beside it. Bootstrap
   spacing utilities are !important, which is why this is. */
#wrapwrap .nix-thread {
  margin-top: 0 !important;
  position: fixed;
  top: var(--shell-gap);
  right: var(--shell-gap);
  bottom: var(--shell-gap);
  z-index: 20;
  width: var(--nix-chat-w);
  /* A COLUMN THAT DOES NOT SCROLL. The panel holds a heading, a thread and a
     composer; if the panel itself scrolls, the composer scrolls off the bottom
     and the heading scrolls off the top, which is every chat window nobody
     has ever built. Only the thread moves — see the three rules under this. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 24px;
  background: var(--nix-panel);
  border-radius: var(--radius-xl);
  transform: translateX(calc(100% + var(--shell-gap)));
  visibility: hidden;
  transition: transform 420ms var(--ease-out-expo), visibility 0s linear 420ms;
}

/* ── The thread fills, the composer sits on the floor ───────────────────────
   THE CHATTER IS IN A SHADOW ROOT, so half of this cannot be written here.
   portal_chatter_service.js creates #chatterRoot, calls attachShadow on it and
   mounts the app inside — which is why #chatterRoot has no children in the
   inspector and why a selector for .o-mail-Chatter written in this file
   matches nothing, quietly, for ever. The rules that reorder the chatter are
   injected INTO that shadow root by static/src/portal/quote_chat.js.

   What is left here is the light-DOM chain the shadow host hangs off: the
   panel, Odoo's #discussion wrapper and the host itself all have to be flex
   columns that fill, or the host has no height to give the app inside it and
   the whole thread collapses to its content. min-height: 0 on each, because a
   flex item's default is auto — "as tall as my content" — which is how a
   scroller ends up not scrolling. */
#wrapwrap .nix-thread > #discussion,
#wrapwrap .nix-thread #chatterRoot {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
}

/* The heading is the panel's, so it does not scroll with the thread. */
#wrapwrap .nix-thread > h3 {
  flex: 0 0 auto;
}

html.nix-chat-open #wrapwrap .nix-thread {
  transform: none;
  visibility: visible;
  transition: transform 420ms var(--ease-out-expo), visibility 0s;
}

/* The document's h3 was written for the width of the page, not for a 380px
   panel: at the page's size the two words ran past the edge and under the
   close control. */
#wrapwrap .nix-thread h3 {
  /* 22 and not 18. The first number was chosen to stop the document's own h3
     running past the edge of a 380px panel and under the close control, and it
     overcorrected: at 18px the heading of a whole panel read smaller than the
     text of the messages under it, which puts the panel's title below its
     contents in the order things are noticed. 22 clears the control and still
     reads as the heading of the column it sits over. */
  font-size: 22px;
  margin: 0 0 16px;
}

/* Absolute, not floated. The panel is a flex column now and a float in a flex
   container is ignored — the control stopped sitting beside the heading and
   became a row of its own above it. */
.nix-chat__close {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--nix-text-soft);
  cursor: pointer;
}

.nix-chat__close svg {
  width: 18px;
  height: 18px;
}

/* ON A DESKTOP THERE IS NO CLOSE. The panel takes width from the page rather
   than covering it, so nothing is hidden behind it and the button in the
   corner is already both doors. A second control that only ever undoes the
   first one is furniture. */
@media (min-width: 721px) {
  .nix-chat__close { display: none !important; }
}

/* ON A PHONE IT IS A BACK. The thread covers the quotation at this width, so
   the reader is somewhere else and needs the way back — in the top-left, where
   a back control lives, and where the rail's ring would otherwise be offering
   a menu for a page that is no longer on screen. The ring stands down and this
   takes its place. */
@media (max-width: 720px) {
  .nix-chat__close {
    top: 16px;
    left: 16px;
    right: auto;
    width: 36px;
    height: 36px;
    background: var(--nix-surface);
    color: var(--nix-text);
  }

  .nix-chat__close .nix-chat__x { display: none; }

  html.nix-chat-open .nix-chat__trigger,
  html.nix-chat-open #site-menu-trigger {
    display: none !important;
  }
}

/* The X is what a desktop would show if it ever showed this control; keeping
   the rule here rather than deleting the glyph means the choice is in one
   place and readable. */
.nix-chat__close .nix-chat__back { display: none; }

@media (max-width: 720px) {
  .nix-chat__close .nix-chat__back { display: block; }
}

/* BELOW 720 IT COVERS, BECAUSE THAT IS WHAT THE RAIL DOES THERE. There is no
   width to give up on a phone — the panel is already the whole screen and .page
   drops its own inset and radius at this width — so the thread arrives over the
   page the way the rail arrives over it, and the page keeps its margin. */
@media (max-width: 720px) {
  #wrapwrap .nix-thread {
    inset: 0 0 0 auto;
    width: 100%;
    border-radius: 0;
    transform: translateX(100%);
  }

  html.nix-chat-open #wrapwrap.nav-shell main.page {
    margin-right: 0;
  }

  html.nix-chat-open .nix-chat__trigger {
    right: calc(var(--shell-gap) + 16px);
  }
}

body.nix-chat-locked {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  #wrapwrap .nix-thread,
  #wrapwrap.nav-shell main.page,
  .nix-chat__trigger {
    transition: none;
  }
}

/* ── What the quotation stopped saying ─────────────────────────────────────
   THE ADDRESS BLOCK. "Invoicing and Shipping Address" restated the name and
   email of the person reading the page, on the page they were sent by name and
   opened from their own inbox. It is the only block here addressed to somebody
   who does not already know the answer.

   YOUR CONTACT. The same salesperson is one row up in the panel and one row
   down in the thread — the whole right-hand side of this page is a way to talk
   to them — so a third copy with its own heading was a card pointing at the
   two controls beside it.

   THE FOOT'S ROW OF BUTTONS. Accept & Sign, Feedback and Reject again, at the
   bottom, under a document that already carries all three at the top of the
   panel where the reader starts. The row goes whole rather than losing two of
   its three: a lone Feedback centred under the totals is a stranger button, not
   a smaller row. */
/* !important, and it is the row of buttons that needs it: the row wears
   .d-flex, Bootstrap display utilities are !important, and a plain
   `display: none` loses to one however specific it is. Measured: the block
   still computed `flex` with the rule in place. */
#quote_content #customer_info,
#sidebar_content > div > div:has(> h6),
#quote_content > div.d-flex.justify-content-center.gap-1.d-print-none {
  display: none !important;
}

/* The lines being bought are the document; they get the panel's card rather
   than sitting loose on it. Only the table: the totals under it are the
   summary OF the card and read as its footer, which is why they are outside
   the border and not in a second box of their own. */
/* THREE CARDS, ONE RULE. What is being bought, what it comes to, and the name
   at the bottom — each is a thing the reader stops at, and each was a run of
   rows on an empty panel with nothing saying where one ended and the next
   began. The totals keep their own box rather than riding inside the lines'
   card: they are the answer TO those lines, and a summary inside the thing it
   summarises reads as one more row.

   Sale Information is gone from #sale_info and the dates that were lifted out
   of it are gone from the heading, so #content has nothing left above the
   lines — which is why the first card is also the top of the document. */
/* THE SURFACE AND THE LIFT ARE THE SITE'S, not this page's own wash. These
   cards were 4% of the page's ink over the panel, which is a step that only
   exists in dark: on paper it computed as a grey box on a near-white panel, and
   the quotation was the one place in this portal where a card was grey while
   every other card on the site — the sign-in box, the store chips, the settings
   rows — was white and lifted.

   --nix-surface and --lift-1 are what the rest of the site uses, and they carry
   the theme with them. In light that is white paper with a real shadow under
   it. In dark, --nix-surface is one step up from the panel and --lift-1 is
   `none`, because a shadow is light being blocked and on #161616 there is no
   light to block — see the note over the lift scale in tokens.css. So the dark
   view keeps the flat step it already had, and light stops being an
   afterthought. */
#quote_content #nix_description,
#quote_content .nix-stage,
#quote_content div[name="sol_table"],
#quote_content #total,
#quote_content #signature,
#invoice_content div[name="sol_table"],
#invoice_content #total {
  padding: 16px 20px;
  border-radius: var(--radius-lg);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

/* The invoice only. On the quotation the grid's gap owns every distance between
   cards, and this margin was adding a second one to it: on a phone the totals
   sat 32px under the line table while every other pair sat 16px apart, which
   read as a section break where there is none. The invoice's blocks are still
   plain flow and still need it. */
#invoice_content #total {
  margin-top: 16px;
}

/* ── The total and the signature stand side by side ────────────────────────
   Full width each, they were two mostly-empty cards stacked under a third:
   the totals hug the right of their box and the signature hugs the right of
   its own, so the page was a column of blank left halves.

   THE TWO ARE NOT SIBLINGS. #total is inside <section id="details"> and
   #signature is beside that section, so nothing can lay them out together as
   they stand. display: contents on the section dissolves it — its children
   become children of the grid — and then the three cards are three grid items
   with the lines spanning both columns and these two sharing the row.

   Only above 720. On a phone two half-width cards are two narrow cards, and
   the totals column would wrap. */
/* ── The document is one column of cards at every width ────────────────────
   The grid below only exists above 720, and under it #content had no layout of
   its own at all: the cards fell back to block flow with no gap, so on a phone
   the description sat flush against the first phase and the contract landed
   wherever Odoo's markup happened to put it — between the plan and the prices,
   which is exactly where the desktop layout had taken it away from.

   One flex column here, one grid there, and the SAME order values read by both.
   That is the whole reason the blocks are ordered rather than numbered: a phone
   and a desktop disagreeing about what comes after the plan is not a layout
   difference, it is two different documents.

   section#details dissolves at every width for the same reason. On a phone its
   children have to be siblings of the description too, or the gap and the order
   stop at the section's edge and the three cards inside it clump. */
#quote_content section#details {
  display: contents;
}

#quote_content #content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  /* stretch, so two cards sharing a row end level. start leaves the shorter of
     them floating with its bottom edge somewhere in the middle of the other,
     which reads as one card broken rather than as two. */
  align-items: stretch;
}

/* Everything that is the width of the document says so once. The two small
   cards say nothing and take one column each, which is a half on a phone and a
   quarter on a desktop. */
#quote_content #nix_description,
#quote_content #nix_stages,
#quote_content div[name="sol_table"],
#quote_content #total {
  grid-column: 1 / -1;
}

#quote_content section#details > div:has(.o_portal_product_document),
#quote_content #signature {
  grid-column: span 1;
}

/* A contract with no signature beside it yet takes the whole width rather than
   standing in half a row with a gap for company. */
@media (max-width: 720px) {
  #quote_content #content:not(:has(#signature)) section#details > div:has(.o_portal_product_document) {
    grid-column: 1 / -1;
  }
}

/* THE BLOCKS ARE ORDERED, NOT NUMBERED, and that is a deliberate change from
   what this grid used to do. Every full-width block used to name its own
   grid-row, which works exactly as long as the set of blocks never changes —
   and it changed twice in a week: a contract card arrived above the lines, then
   a description above that. Each addition meant renumbering everything under it
   and guarding the numbers on :has, because an explicitly placed empty row is
   not free, it is a row plus a gap of blank panel.

   `order` costs none of that. Both flex and grid walk the items in order value,
   so the sequence is written once — description, plan, lines, contract, then
   the signature and the total — and a quotation missing any of them simply has
   fewer cards with nothing left behind. */
#quote_content #nix_description { order: 1; }
#quote_content #nix_stages { order: 2; }
#quote_content div[name="sol_table"] { order: 3; }
#quote_content section#details > div:has(.o_portal_product_document) { order: 4; }
#quote_content #signature { order: 5; }
#quote_content #total { order: 6; }

/* ── ON A PHONE THE MONEY COMES FIRST ─────────────────────────────────────
   Across a panel the three sit on one row and reading order barely matters —
   the eye takes the whole line at once. Stacked, it matters completely: the
   contract and the signature became two full-width cards standing between the
   line table and the figure everything on the page has been building towards,
   and the reader had to scroll past both to find out what it costs.

   So under 720 the total steps up behind the lines it totals, and the two
   documents follow it. Three declarations rather than a second full sequence,
   because only the tail differs; the plan above it is in the same order at
   every width, which is the point of ordering the blocks at all. */
@media (max-width: 720px) {
  #quote_content #total { order: 4; }
  #quote_content section#details > div:has(.o_portal_product_document) { order: 5; }
  #quote_content #signature { order: 6; }
}

/* And the two documents keep each other company on a phone rather than becoming
   two more full-width cards to scroll past. Half a phone is narrow, but a glyph
   over a word and a mark over a word are both small things — this is the one
   pair on the page that does not need the measure. */

@media (min-width: 721px) {
  #quote_content #content {
    grid-template-columns: repeat(4, 1fr);
  }

  /* ── The foot of the document is one row of three ─────────────────────────
     What was signed, who signed it, and what it comes to. The contract used to
     stand on its own line above the lines, which put a file nobody opens twice
     between the plan and the prices; and the totals sat beside a signature
     block that is not there until somebody signs, so half of that row was blank
     for the whole time the quotation was actually being read.

     Four columns rather than three, so the two small cards keep a quarter each
     and the figures keep the half they need — a totals table squeezed into a
     third wraps its amounts under their labels.

     THE SPAN IS GUARDED, because two of the three are conditional. The contract
     card exists only when a product carries a document, and the signature
     exists only after signing, so the totals take whatever is left rather than
     naming a column and leaving a hole where the missing one would have been. */
  #quote_content #total {
    grid-column: span 2;
  }

  #quote_content #content:not(:has(#signature)) #total,
  #quote_content #content:not(:has(.o_portal_product_document)) #total {
    grid-column: span 3;
  }

  #quote_content #content:not(:has(#signature)):not(:has(.o_portal_product_document)) #total {
    grid-column: 1 / -1;
  }

}

/* ── The document has a measure ─────────────────────────────────────────────
   The panel is as wide as the window allows, and the quotation was filling it:
   a product description ran the whole width of a 27-inch screen, and the
   totals block sat a third of a metre away from the lines it totals. A column
   has to be readable, not merely possible.

   One measure for the whole page — the title, the buttons and the document all
   read it — so the heading stays over the middle of the column it introduces
   rather than over the middle of the window. min() and not a bare max-width:
   with the thread open the page is already narrow, and at that point the
   measure has to give way rather than push the document under the panel. */
#quote_content,
#invoice_content,
.o_portal_sale_sidebar > .col-lg-4,
.o_portal_invoice_sidebar > .col-lg-4,
.nix-quote-title {
  width: min(880px, 100%);
  margin-left: auto;
  margin-right: auto;
}

/* The trimmed foot: the cipher, then the signature under it. The bar was a
   row built to hold three things at three edges — brand, cipher, socials —
   and with one of the three left it had nothing to space apart. */
.index-foot--quiet .index-foot__bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.index-foot--quiet .index-foot__sign {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ── The page's own headings, in the page's own type ───────────────────────
   Odoo's frontend bundle sets Inter Tight on its headings, and it is linked
   after this file, so every h2, h3 and h5 the portal draws came out in a
   typeface this site does not use — measured: "Inter Tight" on the quotation's
   title, on Signature and on Communication history, against "PP Neue Montreal"
   everywhere around them.

   inherit rather than naming the stack again: the family is declared once, on
   the document, and a second copy here is a second place to change it. */
.nix-quote-title h2,
#quote_content #signature h5,
#wrapwrap .nix-thread h3 {
  font-family: inherit;
}

/* The thread's heading sits over the thread. It was left-aligned under a close
   control pinned to the right, which on a phone reads as a title that has
   slipped. */
#wrapwrap .nix-thread h3 {
  text-align: center;
}

/* THE BREADCRUMB GOES, AT EVERY WIDTH. Home / Sales Orders / Quotation S00011
   is a trail back through pages the reader did not walk: they arrived here from
   an email, and the line was the first thing above the title. It was hidden
   under 720px only, on the reasoning that a desktop has width to spare — but
   width is not a reason to show a route nobody took, and the desktop quote page
   still opened with a trail above its own title. The block goes with it — :has,
   because what is being hidden is the container the trail sits in, not the list
   itself, and an empty container still carries its own margin. */
#wrapwrap main.page > div.o_portal:has(.o_portal_submenu) {
  display: none !important;
}

/* ── The modals were never dressed ─────────────────────────────────────────
   Pay Order and Validate Order are Bootstrap modals and nothing in this
   stylesheet had ever named one: .modal-content computed rgb(255, 255, 255)
   on rgb(33, 37, 41), the framework's own light default, inside a panel that
   is neither. The Stripe form inside is already dark — stripe_theme.js patches
   its appearance — so the sheet the reader saw was a white frame around a dark
   form, which is worse than either.

   No borders. The header and footer rules are hairlines in a colour, and this
   site's seams are made of space and surface rather than lines. */
#wrapwrap .modal-content {
  background: var(--nix-panel);
  color: var(--nix-text);
  border: 0;
  border-radius: var(--radius-lg);
}

#wrapwrap .modal-header,
#wrapwrap .modal-footer {
  border: 0;
}

#wrapwrap .modal-title,
#wrapwrap .modal-body,
#wrapwrap .modal-body h2,
#wrapwrap .modal-body h3,
#wrapwrap .modal-body h4,
#wrapwrap .modal-body h5 {
  color: var(--nix-text);
}

/* The close is a black SVG baked into a data URI — there is no colour on it to
   change, so it is inverted. grayscale first so a partly-coloured glyph cannot
   come back as its complement. */
#wrapwrap .modal .btn-close {
  filter: grayscale(1) invert(1);
  opacity: 0.7;
}

#wrapwrap .modal .btn-close:hover {
  opacity: 1;
}

/* Odoo's own inputs inside a modal — the signer's name, the country select —
   carry .form-control and .form-select, which are white by default in the same
   way the sheet was. */
#wrapwrap .modal-content .form-control,
#wrapwrap .modal-content .form-select {
  background: color-mix(in srgb, var(--nix-text) 6%, transparent);
  color: var(--nix-text);
  border: 0;
}

#wrapwrap .modal-content .form-control::placeholder {
  color: var(--nix-text-soft);
}

/* THREE ACROSS ON A PHONE. At 390 the three measured 133 + 88 + 120 against a
   312-wide group, so Feedback dropped to a second row on its own — a row with
   one button in it reads as a different kind of control, not as the third of
   three. They share the width instead: flex-basis 0 so the split is by
   proportion and not by label length, min-width 0 so a long label can shrink
   rather than push its neighbours out. */
@media (max-width: 720px) {
  #sale_order_sidebar_button,
  #sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) {
    flex-wrap: nowrap;
    gap: 8px;
    width: 100%;
  }

  #sale_order_sidebar_button > *,
  #sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) > * {
    flex: 1 1 0;
    min-width: 0;
  }

  #sale_order_sidebar_button .btn,
  #sidebar_content > div > .d-flex.gap-2:has(.o_download_pdf) .btn {
    width: 100%;
    padding-left: 8px;
    padding-right: 8px;
    font-size: 13px;
    white-space: nowrap;
  }
}

/* ── The gap over the first card, and who was holding it open ──────────────
   #informations is the row Sale Information and the address block used to sit
   in. Both are hidden, so it measures 0 tall — but it was still taking a grid
   row of its own, and an empty row still costs the grid's gap. With that row
   and the buttons' own trailing space the first card sat 40px below them.

   Hidden outright, and the row numbers close up behind it. The one thing left
   inside it is #transaction_info, a payment-state header Odoo renders while a
   redirect is coming back; this page says the same thing twice over already —
   in the green banner at the top and in Last Invoices — so losing the third
   copy is the price of the row. */
#quote_content #informations {
  display: none !important;
}

#quote_content,
#invoice_content {
  margin-top: 20px;
}

/* ── An overlay covers everything, including the ring ──────────────────────
   A z-index cannot win this, and the first attempt at one made it worse.
   main.page carries view-transition-name: nix-panel, which makes it a stacking
   context of its own at level 0 — so the modal's 1055 is 1055 INSIDE main and
   nothing more, while the ring is a sibling of main. Giving the ring a floor of
   5 put it five levels above the entire page, modal included: hit-tested at the
   ring's centre with the sheet open, the topmost element was the button, over
   .modal-title.

   So it steps aside instead. Bootstrap marks the body while a modal is open,
   and a control for a page that is behind a sheet has nothing to offer from on
   top of it. Same reasoning as the trigger and the ring on a phone when the
   thread covers the quotation. */
body.modal-open #site-menu-trigger {
  display: none !important;
}

/* ── The invoice is the quotation's page, not a printout of one ────────────
   /my/invoices/<id> shipped as an <iframe> holding the PDF report: a white
   sheet with the company logo, the company's own postal address and a five
   column table, dropped into the middle of a dark panel. Everything above and
   below it was this site's; the document itself was a photograph of a piece of
   paper, and no rule in this file could reach inside it — a stylesheet does not
   cross into another document.

   views/portal/invoice.xml takes the iframe out and draws the lines and the
   totals in the page itself, with the same two hooks the quotation uses,
   div[name="sol_table"] and #total, which is why almost every rule this page
   needs is one already written above with #invoice_content added to it. What is
   left here is the handful that is only true of the invoice.

   THE FRAME AROUND IT STAYS. .o_portal_html_view is what
   account_payment.portal_invoice_page_inherit_payment locates to put the Pay
   Now form before, and an inherit whose xpath finds nothing is a module that
   will not install — so the container is emptied and undressed rather than
   removed. bg-white and shadow are Bootstrap utilities and those are
   !important, which is the only reason these two declarations are. */
#invoice_content .o_portal_html_view {
  background: transparent !important;
  box-shadow: none !important;
  overflow: visible;
}

/* THE INVOICE'S ROW IS THE QUOTATION'S ROW. Both pages draw the same three
   controls — a way to keep the document, the one thing to do, a way to ask
   about it — out of two different Odoo sidebars, and for a while they had two
   sets of rules. They were not independent: the quotation's container is also
   a .d-flex.flex-column.gap-2 holding a .o_download_pdf, so the invoice's
   `flex: 0 0 auto` on that wrapper matched it too and out-specified the third
   the quotation was handing out. That is why Download kept coming out narrower
   than the other two however many times the quotation's own rule was fixed.

   One selector, both pages, further down. */

/* WHAT THE PAGE SAYS ABOUT PAYMENT, IT SAYS ONCE. Odoo's answer is a headline
   the width of the panel — the total, again, above the table that itemises it —
   with a badge and a "due in a month" underneath. The total is already the last
   row of the totals card; what is NOT anywhere else on the page is whether this
   has been paid and when it is due, so that is all the chip carries.

   The headline block itself is hidden by the rule the quotation already needed
   (#sidebar_content > .position-relative), which is the same node here. */
.nix-doc-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 4px 12px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--nix-text) 8%, transparent);
  color: var(--nix-text-soft);
  font-size: 13px;
}

/* Paid is the one state that is an answer rather than a status, so it is the
   one that is allowed to be ink rather than a wash. */
.nix-doc-chip--paid {
  background: color-mix(in srgb, var(--nix-text) 14%, transparent);
  color: var(--nix-text);
}

/* THE TOTALS KEEP THE RIGHT HALF, and they have to be told to. On the
   quotation the card shares a grid row with the signature, which is what gives
   it half the width; an invoice has no signature and the same card, left alone,
   ran the whole panel with its two columns of figures pinned to the far right
   of it and a hand's width of nothing to their left.

   Only above 720, for the reason the quotation's grid is: half of a phone is a
   column the figures wrap in. */
@media (min-width: 721px) {
  #invoice_content #total {
    width: calc(50% - 8px);
    margin-left: auto;
  }
}

/* And the block inside it fills its card. Odoo sizes it .col-md-5 with ms-auto
   for a full-width document, so inside a half-width card it was a half of a
   half. ms-auto is a margin utility and those are !important. */
#invoice_content #total > div {
  width: 100% !important;
  max-width: none !important;
  flex: 0 0 100% !important;
  margin: 0 !important;
}

/* ── The document has its own measure, and it is #content ──────────────────
   The cap written for the quotation is on #quote_content and #invoice_content,
   the Bootstrap column each document sits in — and on the invoice it did not
   land: the cards ran the full width of the panel, edge to edge, a line of
   products a metre from the price that belongs to it. The column is
   contested ground (a .row child with Bootstrap's own width, gutters and
   padding on it, plus whatever portal.scss adds on a /my page), and rather
   than out-argue that, the measure moves onto the one element in this document
   that is OURS: #content, written by views/portal/invoice.xml, which nothing
   else styles and nothing else can outrank.

   760 and not the column's 880: 880 was chosen for a quotation that carries a
   signature block beside its totals. An invoice is a table and a summary, and
   at 880 the summary was floating a long way from the lines it adds up. */
#invoice_content #content {
  width: min(760px, 100%);
  margin-inline: auto;
}

/* ── The table brings its own ink, and it is the wrong one ─────────────────
   Bootstrap's .table paints every cell: `color: var(--bs-table-color)` and
   `background-color: var(--bs-table-bg)`, both resolved from variables that
   describe a document on white paper. Inside a dark card that came out as ink
   on ink — Products, Quantity, Essential and the amounts were all there,
   all unreadable, which is what the first screenshot of this page showed.

   Scoped to #invoice_content on purpose. The quotation's table sits in a
   different ancestor chain and reads correctly today, and a page that is right
   is not a page to change while fixing another one.

   background and box-shadow go with the colour: the cell background is the
   body's own, and the inset shadow is how .table paints its striping — both
   would sit on top of the card's wash. */
#invoice_content .table,
#invoice_content .table > :not(caption) > * > * {
  color: var(--nix-text);
  background-color: transparent;
  box-shadow: none;
}

/* The row rules stay as rules, in the ink the rest of this page draws seams
   in. Odoo's border colour here is a light-document grey. */
#invoice_content .table > :not(caption) > * > * {
  border-color: color-mix(in srgb, var(--nix-text) 12%, transparent);
}

/* ── The contract is a card at the top of the quotation ────────────────────
   Odoo draws attached product documents as a "Documents" heading and a strip of
   grey chips, in .bg-light — a light-document utility that came out as a white
   tile on this panel — sitting between the customer block and the line table.
   With a contract attached to every plan, that strip is the most important
   thing on the page after the price, and it was the least visible.

   THE HEADING GOES. "Documents" over a single named file repeats the file, and
   every other heading this document prints is already hidden — Sale
   Information, the intro, the address block. What is left is the file itself,
   in the same card the lines and the totals wear.

   #details is on the <h4> as well as on the <section>, which is Odoo's markup
   and not a typo on our side; the section is dissolved into the grid a few
   rules up and this one names the heading. */
#quote_content h4#details {
  display: none;
}

/* The wrapper keeps nothing but its place in the flow. mb32 is Odoo's own
   32px margin utility and carries !important. */
#quote_content section#details > div:has(.o_portal_product_document) {
  margin-bottom: 0 !important;
  gap: 8px;
}

/* And the chip becomes the card: same wash, same radius and same padding as
   the lines and the totals, full width, so a contract reads as a row of the
   document rather than as a tag beside it. .bg-light is a Bootstrap background
   utility, so the background needs the same weight to be replaced. */
#quote_content section#details .bg-light {
  display: flex;
  flex: 1 1 100%;
  padding: 16px 20px !important;
  /* !important, and it is .rounded that needs it. Odoo puts both .bg-light and
     .rounded on this box, and every Bootstrap utility ships !important — so the
     radius written here lost to Bootstrap's 6px and the contract sat beside
     three 16px cards with corners a third as round. The background above had
     already needed the same weight for the same reason; the radius was missed
     because 6px reads as a rounded corner rather than as a wrong one. */
  border-radius: var(--radius-lg) !important;
  background: var(--nix-surface) !important;
  box-shadow: var(--lift-1);
}

#quote_content section#details .bg-light a {
  align-items: center;
  gap: 12px;
  color: var(--nix-text);
  text-decoration: none;
}

/* The label under the glyph, and the label under the signature, are the same
   thing: a quiet word naming the object above it. One rule, so they cannot
   drift into two sizes. */
#quote_content .o_portal_product_document,
#quote_content #signature h5 {
  flex: 0 0 auto;
  margin: 0;
  font-size: 13px;
  color: var(--nix-text-quiet);
  text-align: center;
}

/* The document's glyph, in the disc the phase icons wear. Same size, same wash,
   same ink — a contract and a phase are two things the reader picks out of the
   page the same way, so they are drawn the same way. */
#quote_content .nix-doc__icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-chip);
  background: var(--tone-bg);
  color: var(--tone-ink);
}

#quote_content .nix-doc__icon svg {
  width: 20px;
  height: 20px;
}

/* ── And the document takes the first row of the grid ───────────────────────
   The rows are named rather than left to the auto-flow, for the reason the
   rest of this grid is: source order would have put the contract wherever a
   cell happened to be free. Everything below it moves down one.

   Guarded on :has, so a quotation with no attached document keeps the two-row
   layout it has today — an explicitly placed empty row is not free, it is a
   row plus a gap of blank panel. */
@media (min-width: 721px) {

}

/* ── The buttons go to the foot of the quotation ───────────────────────────
   They were at the top because Odoo's sidebar was a column beside the document
   and the top of that column was the top of the page. Flattened across the
   panel they became a row above everything — three controls asking for a
   decision before the reader had met the description, the contract, the lines
   or the price.

   So the two children of the flattened row swap: the document first, the
   controls after it. `order` and not markup, for the reason everything else on
   this page is ordered that way — the sidebar is Odoo's template and the row it
   sits in is Odoo's row.

   The gap is the sidebar's, not the document's: #quote_content already carries
   its own top margin for the title above it. */
.o_portal_sale_sidebar > #quote_content {
  order: 1;
}

.o_portal_sale_sidebar > .col-lg-4 {
  order: 2;
  margin-top: 24px;
}

/* The description reads as prose, not as a table row. It is the one card on
   this page whose content is sentences, so it gets the leading a paragraph
   wants and the quiet ink the rest of the document uses for anything that is
   not a figure or a heading. The last paragraph loses its trailing margin —
   Odoo's editor ends every block with one, and inside a card that is a strip of
   empty panel below the text. */
#quote_content #nix_description {
  line-height: 1.6;
}

#quote_content #nix_description p:last-child,
#quote_content #nix_description ul:last-child,
#quote_content #nix_description ol:last-child {
  margin-bottom: 0;
}

/* ── A word longer than the card breaks inside it ──────────────────────────
   A description pasted as one unbroken run of characters — no spaces, which is
   what a reference number, a URL or a paste from somewhere else looks like —
   had nowhere to wrap, so the line ran straight out of its card, past the edge
   of the panel and off the page. Nothing was clipped and nothing scrolled: the
   text was simply somewhere the reader could not reach.

   anywhere and not break-word, and the difference is the whole point. Both let
   a long word break, but only `anywhere` counts those break opportunities when
   the browser works out the box's minimum width — so with break-word the CARD
   would still have been sized to hold the unbroken word and the panel would
   still have been dragged wide by it. This is also why the rule names the
   cells and not just the description: an item called by a part number does the
   same thing to a table column.

   The table keeps word-break: normal on purpose. Breaking mid-word is the last
   resort here, not the first: ordinary prose and ordinary product names still
   wrap at their spaces. */
#quote_content #nix_description,
#quote_content #sales_order_table td,
#quote_content #sales_order_table th,
#invoice_content #invoice_lines_table td,
#invoice_content #invoice_lines_table th {
  overflow-wrap: anywhere;
  word-break: normal;
}

/* ── The phases read as a plan, not as a table ─────────────────────────────
   A phase is a name, a length and a short list of what happens inside it, and
   those three do not want the same treatment: the name and the length are one
   line facing each other across the card, and the rest hangs under them. A
   table would have made the length a column and forced every phase to be as
   tall as the wordiest one.

   ONE CARD PER PHASE, not one card holding five rows. Stacked inside a single
   card the phases were divided by hairlines, and a hairline says "next row of
   the same thing" — which is the wrong reading. These are five pieces of work,
   each with its own length, its own list and its own deadline once the order is
   confirmed; given their own cards they read as five things, and the eye can
   land on one without first measuring where it starts.

   The container keeps the flow and nothing else. Its gap is the same 16px that
   separates the cards below it, so the rhythm down the page does not change at
   the point where the plan is. */
#quote_content #nix_stages {
  /* ONE NUMBER, READ TWICE. The gap between the cards and the length of the
     connector drawn in it are the same distance by definition — a line that
     falls short of either card reads as a mistake rather than as a join — so
     the number is written once here and both rules take it from the property. */
  /* 24 and not the 16 the other cards use. Those are four different blocks of
     one document and 16 is enough to tell them apart; these are five of the
     same thing, stacked, and at 16 they read as one long striped block. The
     connector grows with it, which is the point of the property. */
  --nix-stage-gap: 24px;
  display: flex;
  flex-direction: column;
  gap: var(--nix-stage-gap);
}

/* ── The cards are joined, because the phases are ──────────────────────────
   Five separate cards say five things; they do not say that the second one
   starts when the first one ends. A short rule standing in each gap does, and
   it is the one piece of ornament on this page that carries information: this
   is a sequence, and you are looking at it in order.

   Drawn by the card BELOW the gap rather than the one above, so the last phase
   has nothing hanging off its bottom edge with nowhere to go — a trailing stub
   under the final card is exactly the thing this is meant not to say.

   A pseudo-element and not a real node: it is not content, nothing reads it
   aloud, and QWeb should not be emitting spacers into a document. */
#quote_content .nix-stage {
  position: relative;
}

#quote_content .nix-stage + .nix-stage::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--nix-stage-gap) * -1);
  width: 2px;
  height: var(--nix-stage-gap);
  transform: translateX(-50%);
  border-radius: 1px;
  background: color-mix(in srgb, var(--nix-text) 20%, transparent);
}

/* THE HEAD IS THREE THINGS, and only the middle one is allowed to grow: a glyph
   that says what kind of work this is, the step and its name, and the length.
   center and not baseline — a 40px disc has no baseline worth aligning to, and
   aligning the text to one put the icon a few pixels low against it. */
#quote_content .nix-stage__head {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* ── The phase is a <details>, so the browser owns open and closed ─────────
   THE MARKER GOES, TWICE. A <summary> draws a disclosure triangle, and the two
   engines disagree about what it is: `list-style: none` answers Firefox and the
   standard, ::-webkit-details-marker answers Safari, and neither one alone
   clears both. The chevron on the right is the control, and a triangle in the
   corner beside it is a second one saying the same thing.

   display: flex on the summary is also what suppresses the marker in Chrome —
   a marker has nowhere to sit in a flex container — which is why the head's own
   layout rule above is doing double duty and must not become a grid. */
#quote_content summary.nix-stage__head {
  list-style: none;
  cursor: pointer;
  /* The head is the hit target and the card already has the padding, so the
     click area runs the full width of the card rather than the width of the
     words. */
  user-select: none;
}

#quote_content summary.nix-stage__head::-webkit-details-marker {
  display: none;
}

/* A phase with nothing inside it does not open, so it does not offer to. */
#quote_content .nix-stage--flat .nix-stage__head {
  cursor: default;
}

#quote_content .nix-stage__chev {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  color: var(--nix-text-quiet);
  transition: transform 220ms var(--ease-out-expo);
}

#quote_content .nix-stage__chev svg {
  width: 18px;
  height: 18px;
}

#quote_content .nix-stage[open] .nix-stage__chev {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  #quote_content .nix-stage__chev {
    transition: none;
  }
}

/* ── The one place on this page that is allowed a colour ───────────────────
   THIS SITE IS MONOCHROME AND THAT IS NOT AN ACCIDENT — every surface on it is
   a wash of one ink over one ground, and every rule in this stylesheet down to
   here has held to it. Six tinted tiles are a deliberate exception, and they
   earn it by doing a job nothing else on the page can: five phase cards in a
   column are told apart at a glance by the colour of their corner long before
   anybody reads a heading. The colour is on the tile and nowhere else — no
   coloured text, no coloured borders, no coloured card.

   TWO VALUES PER TONE, ONE PER THEME, and they are not the same colour at two
   opacities. On paper a tint is a pale wash with saturated ink on it; on a dark
   panel the same wash is a grey smudge and the same ink is unreadable, so dark
   gets a translucent ground and a lightened ink of its own. Six tones, twelve
   pairs, written out rather than computed: color-mix guesses at lightness and
   two of the six came out muddy when it did.

   A tile and not a disc: --radius-chip is what this stylesheet calls a tile
   already, and a square with soft corners is what carries a tint. A circle of
   colour beside a heading reads as a status light. */
#quote_content .nix-tone--violet { --tone-bg: #EDEBFE; --tone-ink: #5B4FD6; }
#quote_content .nix-tone--blue   { --tone-bg: #E7EFFD; --tone-ink: #2A63C7; }
#quote_content .nix-tone--green  { --tone-bg: #E4F5EC; --tone-ink: #1B7A4E; }
#quote_content .nix-tone--amber  { --tone-bg: #FAF0E1; --tone-ink: #8F5E0E; }
#quote_content .nix-tone--cyan   { --tone-bg: #E1F1F5; --tone-ink: #0F6C7B; }
#quote_content .nix-tone--sand   { --tone-bg: #F1ECE6; --tone-ink: #75614E; }

html[data-theme="dark"] #quote_content .nix-tone--violet {
  --tone-bg: rgba(139, 124, 246, 0.16); --tone-ink: #B7ACFB;
}
html[data-theme="dark"] #quote_content .nix-tone--blue {
  --tone-bg: rgba(96, 150, 247, 0.16); --tone-ink: #A8C6FA;
}
html[data-theme="dark"] #quote_content .nix-tone--green {
  --tone-bg: rgba(74, 196, 140, 0.16); --tone-ink: #8FE0B7;
}
html[data-theme="dark"] #quote_content .nix-tone--amber {
  --tone-bg: rgba(230, 168, 72, 0.16); --tone-ink: #F0CB8E;
}
html[data-theme="dark"] #quote_content .nix-tone--cyan {
  --tone-bg: rgba(72, 196, 214, 0.16); --tone-ink: #93DDEA;
}
html[data-theme="dark"] #quote_content .nix-tone--sand {
  --tone-bg: rgba(203, 180, 155, 0.16); --tone-ink: #DCC9B4;
}

#quote_content .nix-stage__icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-chip);
  background: var(--tone-bg);
  color: var(--tone-ink);
}

#quote_content .nix-stage__icon svg {
  width: 20px;
  height: 20px;
}

/* min-width: 0, and it is not decoration. A flex item's floor is the width of
   its longest unbreakable word, so without this a phase named after something
   long pushes the duration off the right of the card instead of wrapping. */
#quote_content .nix-stage__titles {
  flex: 1 1 auto;
  min-width: 0;
}

/* 01, 02, 03. Small, wide-tracked and quiet — it is a position in a sequence,
   not a heading, and at full weight it would compete with the phase it counts.
   tabular-nums so the digits sit in the same place down the column, which is
   the only reason a number like this is padded with a zero at all. */
#quote_content .nix-stage__step {
  display: block;
  font-size: 11px;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  color: var(--nix-text-quiet);
}

#quote_content .nix-stage__name {
  display: block;
  font-size: 16px;
  overflow-wrap: anywhere;
}

/* The rest of the card lines up with the name, not with the icon. Indented by
   the disc and its gap so the phase reads as one column of text with a marker
   beside it, rather than as two blocks that happen to share a card. */
#quote_content .nix-stage__body:not(:empty) {
  margin-left: 54px;
  /* The head is the click target and its 40px disc already sets the row's
     height, so the gap under it belongs to the panel that opens, not to the
     control that opens it. */
  margin-top: 12px;
}

/* The duration is a fact about the phase, not a heading of its own: quiet ink,
   and it does not wrap — "12 days" broken across two lines at the right-hand
   edge of a card reads as two different numbers. */
#quote_content .nix-stage__days {
  flex: 0 0 auto;
  white-space: nowrap;
  color: var(--nix-text-quiet);
  font-size: 13px;
}

#quote_content .nix-stage__what {
  color: var(--nix-text-soft);
  line-height: 1.6;
  /* The description is a plain Text field, so its line breaks are characters
     rather than markup and nothing else will honour them. */
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/* The tasks are a list of things that will happen, so they keep their markers —
   this is the one place on the page where a bullet is doing work rather than
   decorating. Indented to the text, not to the card. */
#quote_content .nix-stage__tasks {
  margin: 8px 0 0;
  padding-left: 20px;
  color: var(--nix-text-soft);
}

#quote_content .nix-stage__tasks li {
  margin-bottom: 2px;
  overflow-wrap: anywhere;
}

/* ── EACH BLOCK FILLS ITS OWN CARD, AT EVERY WIDTH ────────────────────────
   Both are Bootstrap columns sized for a full-width document — .col-md-5 with
   ms-auto for the totals, .col-md-4 with ms-auto for the signature — so inside
   a card they are a fraction of a fraction, pinned to the right with the rest
   of the card blank. That is exactly what a phone showed: a signature shoved
   against the right edge of its own card, because these resets used to live
   inside the desktop query and below 720 the .col-sm-7 and the ms-auto were
   still in force with nothing answering them.

   ms-auto is a margin utility and those are !important, which is why the margin
   is reset with the same weight. .row carries negative side margins for its
   gutter too, which pushed the signature 30px past the card it sits in —
   measured 404 inside a box whose content width is 374. */
#quote_content #signature div[name="signature"] {
  margin: 0 !important;
}

#quote_content #total > div,
#quote_content #signature div[name="signature"] > div {
  width: 100% !important;
  max-width: none !important;
  flex: 0 0 100% !important;
  margin: 0 !important;
}

/* ── The contract: the glyph in the middle, the word at the foot ──────────
   The same shape as the Signature card beside it, so the two read as a pair
   rather than as two different ideas that happen to be adjacent. The figure
   grows into whatever the card has spare and centres the tile inside itself;
   the label is left with the bottom edge.

   At every width, not only on a desktop. The two cards share a row on a phone
   now as well, and a narrow card is exactly where a wide row of icon-then-name
   fails worst.

   !important, and it is .flex-row that needs it. Odoo's anchor carries .d-flex
   AND .flex-row, both Bootstrap utilities, both shipping !important — so the
   column written here lost and the glyph sat beside its label instead of above
   it. Same class of defect as the contract's radius and the Download button's
   order: on this page every Odoo element is wearing utilities, and a plain
   declaration against one of them does nothing and says nothing about it. */
#quote_content section#details .bg-light > .position-relative {
  flex: 1 1 auto;
  display: flex;
  text-align: center;
}

#quote_content section#details .bg-light a {
  flex: 1 1 auto;
  flex-direction: column !important;
  align-items: stretch;
  gap: 12px;
}

#quote_content .nix-doc__figure {
  flex: 1 1 auto;
  display: grid;
  place-items: center;
}

/* ── The signature card: the mark in the middle, the word at the foot ──────
   Odoo prints the heading, then the image, then the name typed into the pad —
   which is the order a form fills them in and the wrong order to read them. The
   mark is the thing; the word under it names it; and the typed name is a third
   copy of something the page already says twice, once in the customer block and
   once in the mark itself.

   Ordered rather than moved, because the markup belongs to
   sale.sale_order_portal_content and this file is a stylesheet. */
/* ── HEIGHT: 100% IS A PROMISE ITS PARENT HAS TO KEEP ──────────────────────
   Both small cards asked their content to fill them and both were ignored, for
   the same reason: a percentage height resolves against a parent that HAS a
   height, and every box between the card and the content was auto — Odoo's
   .row, Odoo's column, the anchor's wrapper. So the glyph, the mark and their
   labels all sat at the top of a card with a hand's width of nothing under
   them.

   Flex all the way down instead of percentages. The grid stretches the card,
   the card stretches its row, the row stretches its column, and only then does
   `margin: auto` on the mark have space to centre itself in. Nothing here
   states a height at all. */
#quote_content #signature {
  display: flex;
  flex-direction: column;
}

#quote_content #signature div[name="signature"] {
  flex: 1 1 auto;
  display: flex;
  align-items: stretch;
}

#quote_content #signature div[name="signature"] > [class*="col-"] {
  display: flex;
  flex-direction: column;
  /* align-items and not text-align: turning the column into a flex container is
     what took the centring away — text-align has nothing to centre once the
     image is a flex item rather than inline content. */
  align-items: center;
}

/* auto top and bottom: the image takes every spare pixel around itself, split
   evenly, which centres it and leaves the label sitting on the bottom edge. */
/* !important on both, and it is an inline style that needs it: Odoo writes
   style="max-height: 6rem; max-width: 100%" onto the <img> itself, and an
   inline declaration outranks any selector this stylesheet can write. Nothing
   short of !important reaches it.

   3.25rem and 70%, because the card is a quarter of a panel now rather than a
   third of a document. At 6rem the mark ran the full width of its card and read
   as a headline — the biggest thing in the foot of the page was a squiggle,
   over a total it dwarfed. */
#quote_content #signature img {
  order: 1;
  margin: auto 0;
  max-height: 3.25rem !important;
  max-width: 70% !important;
}

#quote_content #signature h5 {
  order: 2;
}

/* THE TYPED NAME GOES. It is the third time this page says who is signing —
   the quotation is addressed to them and the mark above is theirs. */
#quote_content #signature p {
  display: none;
}

/* AND THE MARK IS VISIBLE IN THE DARK. A signature is captured as black ink on
   a transparent ground, which on a #1A1A1A card is a signature nobody can see —
   it was there and unreadable through every screenshot of this page. Inverted
   rather than recoloured: the image is one colour on nothing, so flipping it
   gives white ink on the same nothing, and no other treatment leaves the
   strokes intact. */
html[data-theme="dark"] #quote_content #signature img {
  filter: invert(1);
}

/* ── A TABLE'S BOTTOM MARGIN IS DEAD SPACE INSIDE A CARD ───────────────────
   Bootstrap gives .table a margin-bottom of one rem, which is right for a table
   standing on a page with something after it and wrong for one that IS the
   card: it left a strip of empty panel under the last rule of the line table
   and under the Total row, so both cards read as bottom-heavy and the padding
   looked uneven against the top. The card's own padding is the space; the table
   does not get to add a second one. */
#quote_content #sales_order_table,
#quote_content #total table,
#invoice_content #invoice_lines_table,
#invoice_content #total table {
  margin-bottom: 0;
}

/* ── The figures get the width they need, the description gets the rest ────
   A table hands its columns out in proportion to what is in them, and this one
   has a paragraph in the first column against two short numbers in the other
   two. The paragraph won: "Quantity" came out as "Qua / ntity" stacked in the
   header, and $4,250.00 broke across three lines as "$ 4,2 / 50.0 / 0" — a
   price the reader had to reassemble.

   width: 1% with nowrap is the old, correct answer and not a hack: a table
   gives a cell at least the width of its unbreakable content, so 1% asks for as
   little as possible and nowrap sets the floor at exactly the number. Every
   spare pixel then falls to the column that has no width of its own, which is
   the description. Naming a fixed rem width instead would be a guess that is
   wrong for a four-figure total or a two-digit quantity.

   Both tables, because both have the same shape: Odoo's on the quotation and
   ours on the invoice. */
#quote_content #sales_order_table #product_qty_header,
#quote_content #sales_order_table #product_unit_price_header,
#quote_content #sales_order_table td[name="td_product_quantity"],
#quote_content #sales_order_table td[name="td_product_priceunit"],
#quote_content #sales_order_table td[name="td_combo_quantity"],
#quote_content #sales_order_table td[name="td_combo_priceunit"],
#invoice_content #invoice_lines_table th:not(:first-child),
#invoice_content #invoice_lines_table td:not(:first-child) {
  width: 1%;
  white-space: nowrap;
}

/* And the description is the column that stretches — but not without end. On a
   wide screen a line of prose running the full measure is a line the eye loses
   its place in on the way back; 60ch is where a reading column stops being one.
   It is a max and not a width, so a narrow panel still gives it everything. */
#quote_content #sales_order_table td[name="td_product_name"],
#quote_content #sales_order_table td[name="td_combo_name"],
#invoice_content #invoice_lines_table td:first-child {
  max-width: 60ch;
}

/* ── On a phone the row of actions stays on screen ─────────────────────────
   The buttons were moved to the foot of the document on purpose — nobody should
   be asked to sign before they have read what they are signing. On a desktop
   that costs nothing: the whole quotation is a screen or two and the row is
   where the reading ends. On a phone the same document is eight screens, and a
   reader who has decided halfway down has to scroll to the bottom to act on it.

   sticky and not fixed, and the difference matters: a fixed bar is pinned to
   the window and would hang over the page from the first screen, including over
   the phases it is asking the reader to consider. Sticky is pinned to its own
   container, so it rides at the bottom of the panel while the document scrolls
   past and comes to rest in its real place at the end. The reader never sees it
   arrive or leave.

   The panel's own colour behind it, because the document scrolls underneath and
   a translucent bar would show line items climbing through the buttons. And the
   safe-area inset, because on a phone with a home indicator the last 34px of
   the window are not somewhere a button can be pressed. */
/* At every width, not only on a phone. A desktop quotation with a plan and six
   phases is no longer the screen-and-a-half it was when the row was put at the
   foot, and a reader who has decided partway down should not have to go looking
   for the button.

   NO BAR BEHIND IT. The row is three buttons and the space between them, and
   that space shows the document going past — which is the point: the reader can
   see they are still reading the same page. It only works because the buttons
   themselves are opaque now; a translucent button over a moving document is
   text on text. */
/* AND THE INVOICE'S ROW DOCKS TOO. Every other rule on this panel names both
   sidebars -- the column direction, the width, the divider that goes, the
   button row itself -- and this one named only the quotation's, so the invoice
   kept its three buttons stranded at the top under the title while the
   quotation's followed the reader down. Same markup, same id inside
   (#sidebar_content), same three controls; there was never a reason for the
   two pages to behave differently, only a selector that stopped one short. */
.o_portal_sale_sidebar > .col-lg-4,
.o_portal_invoice_sidebar > .col-lg-4 {
  position: sticky;
  /* max() AND NOT A SUM. The inset is there so a button is never under a home
     indicator; adding our own breathing room on top of the phone's stacked two
     gaps, and on a browser reporting a generous inset the row came to rest well
     clear of the bottom with a band of document showing underneath. Whichever
     of the two is larger is enough, and on a desktop the inset is zero. */
  bottom: max(8px, env(safe-area-inset-bottom, 0px));
  z-index: 5;
  margin-top: 24px;

  /* ── AND IT IS A CARD, like everything else on this page ────────────────
     The row used to float on nothing, which read as three buttons that had
     come loose from the document rather than as the place where it is acted
     on. It takes the surface, the radius and the lift the description, the
     phases, the lines and the totals all take — the same object, doing the
     last thing the page asks for.

     Capped and centred rather than spanning the panel: a dock as wide as the
     document is a toolbar, and a toolbar is furniture. This is one card that
     happens to stay in view. */
  width: 100%;
  max-width: 512px;
  margin-inline: auto;
  padding: 10px;
  background: var(--nix-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift-2);
}

/* THE ONE SHADOW THIS SITE DRAWS IN THE DARK, and the exception is argued
   rather than assumed. tokens.css sets every lift to `none` on a dark ground
   because a shadow is light being blocked, and on #101010 what the browser
   draws instead is a black smear that reads as dirt around a card — true of a
   card lying ON the page, which is every other card here.

   This one does not lie on the page. It floats over a document that scrolls
   underneath it, and without a shadow the only thing separating the dock from
   the panel is four values of grey. Deeper and softer than the light scale, so
   it reads as distance rather than as an outline. */
html[data-theme="dark"] .o_portal_sale_sidebar > .col-lg-4,
html[data-theme="dark"] .o_portal_invoice_sidebar > .col-lg-4 {
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.4);
}


/* ══════════════════════════════════════════════════════════════════════════
 * THE DOCUMENT LISTS — /my/quotes, /my/orders, /my/invoices
 * ══════════════════════════════════════════════════════════════════════════
   Three pages this sheet never had to dress, because until the Business
   Services rows were drawn nothing in the account linked them. They are
   sale's and account's own routes and they were always live; what they had
   was no door.

   ONE TREATMENT, NOT THREE. All three render through the same two templates —
   portal.portal_searchbar for the title and the filters, portal.portal_table
   for the table and the pager — so every rule here is written once against
   that shared markup. The moment `project` joins the manifest, /my/projects
   and /my/tasks arrive already wearing it, having asked for nothing.

   NOTHING HERE IS A REDRAW. The detail pages behind these lists are already
   this module's (views/portal/sale_order.xml, views/portal/invoice.xml) and
   they say a document is a card on the panel: --nix-surface, --radius-lg,
   --lift-1. A list that opens one of those and looks like stock Bootstrap on
   the way in is the same page changing its mind halfway. These rules spend
   that card on the table and stop. */

/* The title, in the account's voice. Every page in this panel that is ours
   writes h1.nix-login__title — 26px at 300, left, quiet. Odoo's searchbar
   writes a .navbar-brand, which bootstrap sizes as a brand and not as a
   heading, so the three lists opened a size and a weight the rest of the
   account does not use. Matched rather than re-declared from scratch: the
   numbers are .nix-login__title's, and if that voice ever changes this
   follows it by having copied the same two values. */
#wrapwrap.nix-account .o_portal_navbar .navbar-brand {
  margin: 0;
  font-size: 26px;
  font-weight: 300;
  color: var(--nix-text);
}

/* The table is the card, and .table-responsive is what gets it. The card
   cannot go on <table> itself: bootstrap collapses its borders, and a
   collapsed table does not clip its cells to a radius — the corners would
   stay square over a rounded ground. The wrapper is the element the radius
   can hold, and it is also the one that scrolls sideways on a phone, so the
   card travels with the scroll instead of being cut by it.

   Scoped through .o_portal_wrap: that is portal.portal_layout's own
   container, so this reaches Odoo's document lists and nothing else in the
   account that happens to carry a responsive table. */
#wrapwrap.nix-account .o_portal_wrap .table-responsive {
  padding: 4px 16px;
  border-radius: var(--radius-lg);
  background: var(--nix-surface);
  box-shadow: var(--lift-1);
}

/* THE STRIPES COME OFF. bootstrap paints every other row with
   --bs-table-striped-bg, which is a translucent black — on the dark panel it
   is invisible and in light it is a grey ladder this site draws nowhere else.
   The rows are separated by a rule instead, which is the one thing a border
   in this stylesheet is allowed to carry (law 1, check_css).

   The variables are set on the table rather than the rows: they are what
   bootstrap's own selectors read, so overriding them here beats undoing the
   background on each cell at a heavier weight. */
#wrapwrap.nix-account .o_portal_wrap .o_portal_my_doc_table {
  --bs-table-bg: transparent;
  --bs-table-striped-bg: transparent;
  --bs-table-striped-color: var(--nix-text);
  --bs-table-color: var(--nix-text);
  --bs-table-border-color: var(--nix-rule);
  margin: 0;
}

/* The head is the quiet row: it names the columns, it is not one of them. */
#wrapwrap.nix-account .o_portal_wrap .o_portal_my_doc_table > thead th {
  padding: 14px 8px;
  font-size: 13px;
  font-weight: 400;
  color: var(--nix-text-quiet);
}

#wrapwrap.nix-account .o_portal_wrap .o_portal_my_doc_table > tbody td {
  padding: 14px 8px;
  vertical-align: middle;
}

/* AND THE LAST ROW DRAWS NOTHING. A rule under the final row is a line
   floating inside the card with nothing beneath it, which reads as a row
   that failed to render. */
#wrapwrap.nix-account .o_portal_wrap .o_portal_my_doc_table > tbody > tr:last-child > td {
  border-bottom: 0;
}

/* The pager sits under the card, not in it. */
#wrapwrap.nix-account .o_portal_wrap .o_portal_pager {
  margin-top: 16px;
}
