/* Fonts are served by app.py from /fonts/<name>.woff2 (they live beside the old preview). */
@font-face{font-family:"Barlow Semi Condensed";font-weight:500 700;font-display:swap;
  src:url("/fonts/barlow.woff2") format("woff2")}
@font-face{font-family:"DM Sans";font-weight:400 700;font-display:swap;
  src:url("/fonts/dmsans.woff2") format("woff2")}
@font-face{font-family:"JetBrains Mono";font-weight:400 700;font-display:swap;
  src:url("/fonts/jbmono.woff2") format("woff2")}
/* Multitec's palette and faces (multitec.in). The condensed display face is used the
   way THEY use it - for headings only. Everything you actually read is DM Sans at a
   size a person can read. Figures are JetBrains Mono so columns line up. */
/* ---------------------------------------------------------------- THE PALETTE, BOTH THEMES
   Amit: "can we have the shole app have a dark mode switcher? will it work? can users choose?"
   It works because of what was already here: 964 var() uses against 123 colour literals, so a
   theme is a second VALUE for tokens that exist rather than a second stylesheet. The plan and
   the eleven families the literals collapsed into are in docs/RULES.md §7b.

   EVERY COLOUR IS A TOKEN NOW, and the ones below the fold are the reason this could not be
   done by overriding the seventeen that were here before. `#E4C489` appeared thirteen times as
   "the amber border" and `#5C3D00` five times as "amber text on an amber wash" - names neither
   of them had, so neither could be given a dark value. A literal in a rule is a colour that
   cannot have a second opinion.

   WHAT STAYS LITERAL, DELIBERATELY: `#fff` where it sits on a coloured SOLID (a red button, an
   ink chip, the amber due badge). White on Multitec red is white on Multitec red in any theme,
   and tokenising it would invite somebody to darken it. `background:#fff` is a different thing
   and is now var(--bg).

   THE PDF IS NOT DOWNSTREAM OF ANY OF THIS. Amit: "i hope the pdf renderer for the quote will
   not show dark mode pdf." It cannot: render/pdf.py and render/grid.py build their own inline
   <style> and never load this file - grep the render/ directory for `app.css` and there is no
   hit, no `data-theme` and no `prefers-color-scheme`. The customer's document has one palette
   and it is not stored here. */
:root{
  color-scheme:light;
  /* FOUR SURFACES, AND THE ORDER BETWEEN THEM IS THE WHOLE OF WHY DARK MODE LOOKED WRONG.
     `--soft` was doing two jobs that only agree in light: it was the PAGE behind the cards AND
     the HOVER on top of them. In light both want "one step off white", so one token served.
     In dark they point opposite ways - the page must be DARKER than a card and a hover must be
     LIGHTER - and sharing a token meant the cards came out darker than the page they sat on.
     Every control then looked sunken and its hairline had nothing to separate it from, which
     is what Amit saw: "the buttons on the side pane, they merge to the background."
     So: --page behind everything, --bg the surface, --soft the hover, --soft-2 the step above.
     In LIGHT the numbers are exactly what they always were; only the names got honest. */
  --page:#F7F8F8;
  --bg:#FFFFFF; --soft:#F7F8F8; --soft-2:#EFF0F1;
  --ink:#141417; --muted:#4E4E56; --dim:#83838B;
  --line:#E6E7E9; --line-strong:#D2D3D6;
  --ink-bg:#0C0C0E;
  --red:#CC211E; --red-bright:#E23A33; --red-wash:#FDF2F1;
  --green:#0E7150; --green-wash:#EDF6F2;
  --amber:#8A5A00; --amber-wash:#FCF4E6;
  --accent:#CC211E;
  /* The three status families each need a BORDER and an INK of their own. A wash alone cannot
     carry a pill: the border is what separates WARM from QUOTED at a glance, and the ink is
     what stays readable on top of the wash. */
  --red-line:#F0BEBC;   --red-ink:#8A1513;   --red-soft:#FFC9C7;
  --green-line:#A9D3C0; --green-ink:#0B5B41;
  --amber-line:#E4C489; --amber-ink:#5C3D00; --amber-wash-2:#FDF6E7;
  /* THE FOURTH STAGE COLOUR. `cold` is blue and had no token at all - four literals spread over
     the stage pill, the reports bar and the timeline's `send` dot, which is how a stage can
     come to be three slightly different blues. */
  --blue:#2F5FA8; --blue-wash:#EFF3FA; --blue-line:#BFD0E7;
  /* WhatsApp's own green. A BRAND colour, not a status one - it means "this button is
     WhatsApp", so it holds its hue in both themes and only its wash gives way. */
  --wa:#25D366; --wa-ink:#128C4A; --wa-wash:#F2FBF6;
  /* A TRIPLET, NOT A COLOUR, and that is what makes twenty-one shadows one decision. They run
     from .02 to .6 and no single rgba() token can serve them; `rgba(var(--shade),.42)` lets
     every one keep its own alpha while the HUE follows the theme. On a dark ground a black
     shadow is invisible and a black scrim cannot be told from the page, so it has to move. */
  --shade:20,20,23;
  --red-rgb:204,33,30;
  /* MULTITEC'S RED, AS A FILL, IN BOTH THEMES - and it is a separate token from `--red` on
     purpose. `--red` is red-as-TEXT and has to lift to #FF6B65 on a dark ground to stay
     legible; painting a whole button in that lifted red gave a washed-out salmon CTA that no
     longer looked like the brand's button. A fill and a letterform want different reds. Same
     argument as the white header: the brand does not change colour at sunset. */
  --red-solid:#CC211E; --on-red-solid:#FFFFFF;
  /* WHAT READS ON A SATURATED SOLID - one token for all four families, and it works because
     the two themes are consistent with themselves: in LIGHT every solid (--ink, --red,
     --amber, --green) is dark, and in DARK every one of them is light. So "the opposite of a
     solid" is a single answer per theme rather than four.
     NOT for --ink-bg, which is near-black in BOTH themes: the four #fff left in this file all
     sit on it, and they are literal on purpose. */
  --on-solid:#FFFFFF;
  --sans:"DM Sans",-apple-system,"Segoe UI",sans-serif;
  --display:"Barlow Semi Condensed","DM Sans",sans-serif;
  --mono:"JetBrains Mono",ui-monospace,Menlo,monospace;
  --maxw:1240px;
}

/* ---------------------------------------------------------------------------- DARK
   ONE TRIGGER, NOT TWO. There is no `@media (prefers-color-scheme: dark)` block here on
   purpose: `system` is RESOLVED to a real word before the page paints (index.html's boot
   script reads matchMedia and writes `data-theme="dark"`), so this file has exactly one rule
   to keep correct instead of two that are free to drift. It also means "System" and "Dark"
   cannot disagree about what dark looks like, and that an explicit Light beats a phone set to
   dark without a specificity argument. That is the whole of "can users choose".

   THE SURFACES ARE NOT A PHOTOGRAPHIC NEGATIVE. Pure black with pure white text vibrates on an
   OLED phone, which is where Amit reads this; the ground is a near-black with a hint of the
   brand's own coolness and the text is a soft white, which is what every well-made dark UI
   does. `--soft` and `--soft-2` get LIGHTER than `--bg` rather than darker, because in dark a
   raised surface catches more light, not less - inverting their relationship is the single
   most common way a dark theme ends up looking inside-out.

   THE STATUS WASHES ARE TINTS, NOT PALE PAPER. #FCF4E6 on a near-black ground is a bright
   rectangle; the pill has to be a low-alpha wash of its own hue with the ink lifted to stay
   legible on it. A stage is read at a glance and never letter by letter, so four pills that
   collapse into four grey lozenges is a stage MISREAD - a wrong business fact on his pipeline,
   which is why these six lines got measured rather than eyeballed. */
:root[data-theme="dark"]{
  color-scheme:dark;
  --page:#121316;                    /* the floor */
  --bg:#1E2024;                      /* a card, a row, a control - raised off the floor */
  --soft:#282A2F;                    /* hover: one step ABOVE the surface, never below */
  --soft-2:#33363D;                  /* pressed, and the raised chip */
  --ink:#ECECEE; --muted:#B4B5BC; --dim:#8A8B93;
  --line:#2E3036;
  /* A CONTROL'S OWN EDGE, LIFTED. Amit, on the quote peek: "i meant the buttons on the side
     pane, they merge to the background." They did: a `--line` hairline is 1.3:1 against this
     ground, which is right for a ROW DIVIDER (a table with loud rules is a table you cannot
     read down) and wrong for the outline that is the only thing telling you a button is there.
     In light the two jobs happen to want nearly the same grey; in dark they part company, and
     the button's edge is the one that has to win. */
  --line-strong:#4A4E57;
  /* A RAISED SURFACE, NOT BLACK. Amit, looking at this: "the black buttons on dark mode are
     not easily reognizable" - and he is right, this was #000000. In light, `--ink-bg` means
     "darker than the page, so it stands out"; the naive translation keeps it black and it then
     means "the same as the page, so it vanishes" - a MULTITEC chip and a toast both dissolved
     into the background. In dark the thing that stands out is LIGHTER, so it lifts to a step
     above the page. White text stays valid on it, which is why every #fff left in this file
     could stay literal. */
  --ink-bg:#383B43;
  --red:#FF6B65; --red-bright:#FF8580; --red-wash:#3A1F1E;
  --red-line:#6B3330; --red-ink:#FFB3AF; --red-soft:#FFC9C7;
  --green:#4CC79A; --green-wash:#12312A; --green-line:#2A5C4A; --green-ink:#8FE0C0;
  --amber:#E0A93C; --amber-wash:#33280F; --amber-wash-2:#3A2E13;
  --amber-line:#5E4A1C; --amber-ink:#F0CE8A;
  --blue:#7BA6E8; --blue-wash:#1A2438; --blue-line:#33456B;
  --wa:#25D366; --wa-ink:#5BE08F; --wa-wash:#132A1D;
  --accent:#FF6B65;
  --shade:0,0,0;
  --red-rgb:255,107,101;
  --red-solid:#CC211E; --on-red-solid:#FFFFFF;
  /* The ground, not white: every solid above is a LIGHT colour here, so white text on the red
     button would sit at about 2.5:1 and the amber badge would be worse. */
  --on-solid:#17181B;
}
*{box-sizing:border-box}
[hidden]{display:none!important}
html,body{height:100%;overflow:hidden}
body{margin:0;background:var(--page);color:var(--ink);font:400 16px/1.55 var(--sans);
  -webkit-font-smoothing:antialiased;display:flex;flex-direction:column}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums}
.lbl{font-size:12px;font-weight:500;color:var(--dim)}
header{flex:0 0 auto;z-index:30;background:var(--bg);border-bottom:1px solid var(--line);
  box-shadow:0 1px 0 rgba(var(--shade),.02),0 10px 30px -24px rgba(var(--shade),.35)}
header::before{content:"";display:block;height:3px;background:var(--red)}
/* THE HEADER STAYS WHITE IN DARK MODE. Amit: "even if we go dark mode, the header nav where
   the logo is should be in white."

   It is not only his taste, it is the asset: the Multitec wordmark is red on transparent, and
   red on a near-black ground is the one thing this palette could not have fixed without a
   second logo file. Keeping the bar white keeps the logo on the ground it was drawn for, and
   the red rule above it still reads.

   IT RE-DECLARES THE LIGHT TOKENS RATHER THAN HARDCODING THE RULES, so everything INSIDE the
   bar - the nav words, the identity button, the due badge - inherits a consistent light
   context instead of each needing its own exception. A `background:#fff` here with dark text
   still cascading into it would have been a white bar with white words on it.

   THE PHONE'S NAV DRAWER AND THE IDENTITY MENU GO WITH IT, and that is the right call rather
   than an accident of the cascade: both are the header's own furniture, they are opened FROM
   the white bar, and he said "the header nav ... should be in white" - the drawer IS the nav.
   A dark panel sliding out of a white bar would be the bar disowning its own menu. */
:root[data-theme="dark"] header{
  /* FOUR SURFACES, AND THE ORDER BETWEEN THEM IS THE WHOLE OF WHY DARK MODE LOOKED WRONG.
     `--soft` was doing two jobs that only agree in light: it was the PAGE behind the cards AND
     the HOVER on top of them. In light both want "one step off white", so one token served.
     In dark they point opposite ways - the page must be DARKER than a card and a hover must be
     LIGHTER - and sharing a token meant the cards came out darker than the page they sat on.
     Every control then looked sunken and its hairline had nothing to separate it from, which
     is what Amit saw: "the buttons on the side pane, they merge to the background."
     So: --page behind everything, --bg the surface, --soft the hover, --soft-2 the step above.
     In LIGHT the numbers are exactly what they always were; only the names got honest. */
  --page:#F7F8F8;
  --bg:#FFFFFF; --soft:#F7F8F8; --soft-2:#EFF0F1;
  --ink:#141417; --muted:#4E4E56; --dim:#83838B;
  --line:#E6E7E9; --line-strong:#D2D3D6;
  --red:#CC211E; --red-bright:#E23A33; --red-wash:#FDF2F1;
  --red-line:#F0BEBC; --amber:#8A5A00; --amber-wash:#FCF4E6;
  --green:#0E7150; --green-wash:#EDF6F2;
  --on-solid:#FFFFFF; --shade:20,20,23;
  color-scheme:light;
  background:var(--bg);
}
/* The header carries APP things only - logo, app name, nav, who you are. The machine
   (model/width/colours) is a fact about the QUOTE, so it lives on the quote: Tab 1 and
   Tab 3, same state, two renderings. Amit: "why does the header still have the model
   number etc. it should be in the quote". */
/* Three columns: brand | nav | session. The outer two are flex:1 with basis:0 so they weigh
   exactly the same and the nav sits on the page's centre line, whatever the sides say. */
/* `flex-wrap` IS THE OVERLAP GUARD, and it is here because the header overlapped for real:
   the flattened nav is seven items, the brand was `flex:1 1 0` with `min-width:0`, and when
   the row ran out of space the brand obeyed and shrank to WIDTH ZERO - so the logo painted
   on top of "Contacts" and "Sales" sat in the middle of the menu. Nothing warned; a flex item
   that is allowed to reach zero just does.
   Two rules stop it for good: nothing in this bar may shrink below its content (`0 0 auto`
   on the two blocks that were `1 1 0`), and if the row still cannot hold everything it WRAPS
   rather than overlapping. A header on two lines is a fit problem; a header on top of itself
   is a bug. */
/* BACK TO --maxw, and the widening it replaced was never the right fix. The header was 27px
   short of holding everything, and the reason was the identity control: four names side by
   side took 383px of a 1240px bar. Collapsing that to one trigger with a menu gave back more
   than 200px, so the row fits at the page's own width with room to spare - and the header
   lines up with the content under it again, which is what it should always have done.
   Amit, on the wider bar: "OMG, you just increased the width of the Nav bar, it was not
   needed." He was right: it was a symptom being paid for with layout. */
.bar{display:flex;align-items:center;gap:20px;padding:14px 28px 16px;
  max-width:var(--maxw);margin:0 auto;flex-wrap:wrap}
/* EQUAL WEIGHTS BOTH SIDES, so the nav lands on the page's TRUE centre and stays there when
   the sides change width - they do, "Gaurav · Multitec" is wider than "Martin". Amit: "the nav
   bar looks stuck to the logo. center it."
   `min-width:max-content` is the part that must never be dropped again: this was `flex:1 1 0`
   with `min-width:0` once, and when the row ran short the brand obeyed and shrank to WIDTH
   ZERO - the logo painted straight over the word "Contacts". Equal weights do the centring;
   the min-width stops either side being centred out of existence. */
.bar > .brand{flex:1 1 0;min-width:max-content}
/* .f is a column, so the right-hand block right-aligns with align-items, not justify.
   min-width:max-content: the identity toggle (a .seg with width:max-content) must never
   overflow its column onto the Due bubble to its left - it did once the labels grew to
   "Gaurav · Multitec" / "Amit · Admin". Reserving the column's content width keeps the
   toggle inside its own box; the flex:1 brand still absorbs the slack, so the nav stays put. */
.bar > .sess{flex:1 1 0;min-width:max-content;align-items:flex-end}
/* THE LOCKUP. Checked against Linear, Stripe and Vercel before touching this again, because
   the first two attempts were both wrong (Amit: "omg - the sales word is so strecthed now.
   please look at some good ui examples").
   What all three actually do: a SMALL wordmark (~26px), and nothing clever near it. The
   earlier version justified SALES to the logo's exact width - a trick that sounds designed
   and reads as a gimmick, because a 5-letter word spread over 163px stops being a word.
   So: SALES is small, quiet, and right-aligned to the wordmark's edge - a sub-brand under a
   brand, the way BMW sets "M" or Adobe sets a product name. Tracking is a normal .24em,
   chosen by eye, not derived from anything. The restraint IS the design. */
/* THE LOCKUP - fourth attempt, and the first three failed for one reason worth writing down.
   "Sales" UNDER the wordmark cannot be made to work: the Multitec mark is italic, heavy and
   slanted - it has motion - and a small upright word sitting flat beneath it fights that
   posture. Tracking it out (attempt 2) or justifying it to the logo's width (attempt 3) only
   made a gimmick out of a mismatch. Amit's verdict on both: "shabby ai work".
   So it goes BESIDE the mark, split by a rule - what GitHub does with owner/repo and Vercel
   with project names. Same baseline, related weights, the rule doing the joining. Sentence
   case and the body face, because it is a product name, not a second brand shouting.

   .brand is the header's LEFT COLUMN (flex:1) and only pushes the mark to the left edge; the
   two boxes stay separate because collapsing them let the logo drift to the centre with the
   nav. No margins on a flex:1 column - margin is width, and width on one side shoves the
   "centred" nav off centre by half of it. .bar's gap does the spacing. */
/* THE LOCKUP - FIFTH ATTEMPT, and the reason it changed is not that the last one was ugly.
   It was right for a five-item nav sitting on the page's centre line. The nav is seven items
   now and starts much closer to the mark, and "Sales" beside it - same baseline, same size
   family, one rule between them - stopped reading as a sub-brand and started reading as the
   first menu item. Amit: "the sales word should have been under Multitec logo otherwise
   people think thats a part of menu too".

   WHY THIS IS NOT ATTEMPT 2 OR 3 AGAIN. Those failed by doing something TO the word: tracking
   it out to the logo's width, then justifying it. Both made a gimmick out of the mismatch
   between an italic, slanted, heavy mark and a small upright word ("shabby ai work"). The
   mismatch is real and the answer is to stop competing with it - so this is just a small,
   quiet, normally-tracked word sitting at the mark's left edge, closer to a caption than to a
   second brand. Nothing is stretched, aligned-to, or derived from the logo's dimensions.

   It also buys back the horizontal room the seventh nav item needed, which is the sort of
   thing that makes a layout decision easy to keep. */
.brand{display:flex;justify-content:flex-start;flex:0 0 auto}
.mark{display:flex;flex-direction:column;align-items:flex-start;gap:2px}
.mark b{font-family:var(--display);font-weight:600;font-size:22px;letter-spacing:.01em;
  text-transform:uppercase;line-height:1}
/* THE MARK STANDS ALONE - no product word beside or beneath it. Five versions of that word
   were tried ("Sales" beside with a rule, tracked out, justified to the logo's width, "CRM"
   spread across it, "CRM" tucked under) and Amit rejected every one, the last as "the word CRM
   looks like shit... it does not look like a professional app".

   HE WAS RIGHT, AND THE WORD WAS ALWAYS THE PROBLEM. The Multitec mark is italic, heavy and
   slanted; anything small, upright and grey placed next to it either fights that posture or
   apologises for itself, and a three-letter initialism has no good size - big enough to have
   presence and it competes with the brand, small enough to defer and it reads as a caption
   nobody asked for. Meanwhile a word sitting where a nav item could sit gets read as one, which
   is what he objected to the first time.

   So it goes. The nav says which section you are in and the browser tab says which app this is;
   the header does not need to repeat either. Linear, Stripe and Notion all do exactly this - a
   mark and nothing else - and the reason is the same one that applies here: a logo with room
   around it looks like a product, and a logo with a label bolted on looks like a wrapper.
   The mark gets the full 40px it was competing for. */
.mark .logo{height:36px;width:auto;display:block}
/* app shell nav: CRM | Quotes | Admin - in that order, and Admin only for the admin.
   Minimal, in the house style - a red underline marks where you are, nothing else. */
/* The nav sits on the CENTRE LINE OF THE PAGE (Amit: "bring the menu in the center of the
   page, its sticking to the logo now"). The brand and the session block are flex:1 with a
   0 basis, so they weigh the same and the nav lands on the true centre - and STAYS there when
   the sides change width, which they do ("Signed in as Agent (Ravi)" is wider than "Amit").
   Centring it with a margin would let it drift.

   The treatment is copied from what good apps actually do, not invented. Linear, Stripe and
   Vercel all set their nav as PLAIN TEXT in sentence case: no box, no border, no uppercase,
   no heavy tracking. An earlier draft put a bordered pill-group here; beside a red italic
   wordmark it read as two competing objects. The current section gets a soft pill - enough to
   say where you are, quiet enough to stay out of the way. */
/* THE NAV IS ONE LINE NOW - seven items where there were five, because Contacts, People and
   Opportunities came up a level out of the CRM tab. `flex-wrap` and `min-width:0` are what
   keep that honest between 721px and about 1100px: the row gives before the header does, so
   a narrow laptop wraps the nav rather than pushing the sign-in block off the edge. */
/* `flex:1 1 auto` IS WHAT PUTS THE SIGN-IN BLOCK ON THE RIGHT EDGE. Everything in this bar is
   `0 0 auto` so nothing shrinks below its content; that also means nothing absorbs the slack,
   and the whole row packed to the left with the identity control stranded mid-header. The nav
   is the one item that can honestly take the extra space - it is a list, not a fixed object -
   so it grows, and the brand ends up hard left and the avatar hard right where Amit wants it.
   `margin-left:auto` on the sess block would do it too, and did it wrongly: the moment the bar
   wraps, an auto margin shoves that block across the row it wrapped onto. */
/* `0 0 auto`: the nav is sized by its own words, and the two flexible sides either side of
   it do the centring. Letting it GROW is what pinned it to the left against the logo. */
.nav{display:flex;gap:4px;flex-wrap:wrap;min-width:0;flex:0 0 auto}
/* 14px and 10px of side padding, down from 15px and 14px: seven items have to live in the
   room five used to, and the alternative was the nav wrapping to a second row on a perfectly
   ordinary 1440px laptop. Measured, not guessed - at 15/14 the row wanted 687px and only
   613px was going spare. */
.nav button{border:0;background:transparent;color:var(--dim);cursor:pointer;
  font-family:var(--sans);font-weight:500;font-size:14px;letter-spacing:.01em;
  padding:7px 10px;border-radius:7px;line-height:1;
  transition:color .12s ease, background-color .12s ease}
.nav button:hover{color:var(--ink);background:var(--soft)}
.nav button[aria-current=page]{color:var(--ink);background:var(--soft-2);font-weight:600}

/* ---------- the hamburger ----------
   HIDDEN ABOVE THE PHONE, and it is `display:none` rather than `visibility` so it takes no
   space in the header's flex row. The three bars are one element and two pseudo-elements -
   no icon font, no inline SVG per instance, nothing to 404 (the wordmark already taught us
   what a missing asset costs in a header). */
.navtoggle{display:none;border:1px solid var(--line-strong);background:var(--bg);
  border-radius:7px;width:38px;height:38px;cursor:pointer;padding:0;
  align-items:center;justify-content:center;flex:0 0 auto}
.navtoggle:hover{border-color:var(--ink)}
.navtoggle .bars,.navtoggle .bars::before,.navtoggle .bars::after{
  display:block;width:17px;height:2px;background:var(--ink);border-radius:2px;content:""}
.navtoggle .bars{position:relative}
.navtoggle .bars::before{position:absolute;top:-5px}
.navtoggle .bars::after{position:absolute;top:5px}
.navtoggle[aria-expanded=true]{background:var(--soft-2);border-color:var(--ink)}
.navscrim{display:none}
.f{display:flex;flex-direction:column;gap:5px}
select,input,button{font-family:var(--sans)}
select:focus-visible,input:focus-visible,button:focus-visible,[contenteditable]:focus-visible{
  outline:2px solid var(--red-bright);outline-offset:1px}
.seg{display:flex;border:1px solid var(--line-strong);border-radius:4px;overflow:hidden;width:max-content}
.seg button{border:0;background:var(--bg);color:var(--muted);font-size:14px;font-weight:500;
  padding:7px 13px;cursor:pointer}
.seg button:hover{background:var(--soft)}
.seg button[aria-pressed=true]{background:var(--ink);color:var(--on-solid)}
main{flex:1 1 auto;min-height:0;width:100%;max-width:var(--maxw);margin:0 auto;
  display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:26px;padding:24px 28px 0;
  align-items:stretch;overflow:hidden}
.docs{min-width:0;overflow-y:auto;overscroll-behavior:contain;padding-bottom:60px}
@media(max-width:1040px){
  html,body{overflow:auto;height:auto}
  main{grid-template-columns:1fr;overflow:visible;padding-bottom:60px}
  .docs,.rail{overflow:visible}
}
.sheet,.cover,.card{background:var(--bg);border:1px solid var(--line);border-radius:6px}
.cover{margin-bottom:18px;overflow:hidden}
.sheet{overflow:hidden}
.sheet + .sheet{margin-top:18px}
.sec{display:flex;justify-content:space-between;align-items:center;gap:16px;
  padding:16px 20px 12px;border-bottom:2px solid var(--ink);background:var(--bg)}
.sec + .sec{border-top:1px solid var(--line)}
.sec h2{margin:0;font-family:var(--display);font-weight:600;font-size:21px;letter-spacing:.02em;
  text-transform:uppercase;color:var(--ink)}
.sec .ctl{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--muted);flex-wrap:wrap;justify-content:flex-end}
.sec .lbl{font-size:12px}
.cols{display:grid;grid-template-columns:24px 1fr 66px 112px 124px;gap:14px;padding:9px 20px;
  background:var(--soft);border-bottom:1px solid var(--line);color:var(--dim)}
.cols span{font-size:12px;font-weight:500}
.cols .r{text-align:right}
.row{display:grid;grid-template-columns:24px 1fr 66px 112px 124px;gap:14px;align-items:baseline;
  padding:11px 20px;border-bottom:1px solid var(--line)}
.row:hover{background:var(--soft)}
.row.on{background:var(--bg)}
.row.free{background:var(--green-wash)}
.row.head{align-items:center;padding:16px 20px}
.row.total{background:var(--soft);border-top:1px solid var(--line-strong);border-bottom:0}
.row.total .t{font-family:var(--display);font-weight:600;font-size:17px;letter-spacing:.03em;
  text-transform:uppercase}
.row input[type=checkbox]{width:17px;height:17px;accent-color:var(--red);margin:0}
/* THE DISCOUNT TOGGLE, under the negotiated total. Deliberately NOT a .row.total: it
   decides what the document SAYS, not what anything costs, and giving it the totals' weight
   would read as a third price. Its hint (.why) carries the three figures. */
.row.disctog{border-bottom:0;padding-bottom:5px}
.row.disctog label{display:inline-flex;align-items:center;gap:9px;cursor:pointer;
  font-size:15px;color:var(--ink)}
.t{font-size:16px;min-width:0;color:var(--ink);line-height:1.5}
.t.b{font-family:var(--display);font-weight:600;font-size:22px;letter-spacing:.01em}
.row:not(.on) .t{color:var(--muted)}
.why{display:block;font-size:13px;color:var(--dim);margin-top:3px}
.tag{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  padding:2px 7px;border-radius:3px;margin-right:7px;color:var(--on-solid);vertical-align:1px}
.tag.free{background:var(--green)} .tag.edit{background:var(--amber)}
.tag.add{background:var(--red-solid);color:var(--on-red-solid)}
.tag.special{background:var(--ink)} .tag.pend{background:var(--amber)} .tag.ok{background:var(--green)}
.q{width:100%;text-align:center;padding:5px;font:500 15px var(--mono);color:var(--ink);
  background:var(--bg);border:1px solid var(--line-strong);border-radius:4px}
.q.zero{color:var(--dim);background:var(--soft)}
.p,.tot{text-align:right}
.p{padding:0}
.pin{width:100%;text-align:right;font:500 16px var(--mono);font-variant-numeric:tabular-nums;
  color:var(--ink);background:transparent;border:1px solid transparent;border-radius:4px;padding:4px 7px}
.pin:hover{border-color:var(--line-strong);background:var(--soft)}
.pin:focus{outline:2px solid var(--red-bright);background:var(--bg)}
.was{display:block;font-family:var(--mono);font-size:12px;color:var(--amber);text-decoration:line-through}
.tot{font-family:var(--mono);font-weight:600;font-size:16px}
.dash{color:var(--line-strong)}
.blk{border-bottom:1px solid var(--line);padding:4px 20px 14px}
.blk.off{opacity:.42}
.blk h3{display:grid;grid-template-columns:1fr 124px;align-items:center;gap:14px;margin:16px 0 6px;
  font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--dim)}
.blk h3 label{font-family:var(--sans);font-size:13px;font-weight:400;color:var(--muted);
  text-transform:none;letter-spacing:0;margin-left:12px}
.blk ul{margin:0;padding:0;list-style:none}
.blk li{font-size:15px;color:var(--muted);padding:4px 0 4px 16px;line-height:1.55;position:relative}
.blk li::before{content:"";position:absolute;left:0;top:13px;width:7px;height:1px;background:var(--line-strong)}
.blk li.pick{display:grid;grid-template-columns:1fr 128px 124px;gap:14px;align-items:center;
  padding:9px 0 9px 16px}
.blk li.pick .picker{width:100%}
.blk li.pick::before{top:19px}
.opt{color:var(--ink)}
.picker{font-size:14px;font-weight:500;color:var(--ink);background:var(--bg);
  border:1px solid var(--line-strong);border-radius:4px;padding:5px 8px;cursor:pointer}
.picker:hover{border-color:var(--red)}
.delta{white-space:nowrap;font-family:var(--mono);font-weight:700;font-size:16px}
.delta.up{color:var(--green)} .delta.down{color:var(--red)}
.pdfnote{font-size:13.5px;color:var(--muted);padding:11px 20px;border-bottom:1px solid var(--line);
  background:var(--amber-wash)}
/* THE DRYING STRIP - the S3S-670's print units, laid out like the press itself: PU1 on
   the left through to the last unit on the right, so the form reads the way the machine
   is built. It scrolls sideways rather than wrapping, because a wrapped press is not a
   press - and a 16-colour machine must not reflow the page. No prices here, ever: the
   server does not send them. */
.dryhead{display:flex;align-items:center;gap:12px;padding:12px 20px 0}
.dryhead select{font-size:15px;color:var(--ink);background:transparent;
  border:0;border-bottom:1px solid var(--line-strong);padding:4px 1px;cursor:pointer}
.dryhead select:focus{outline:0;border-bottom:2px solid var(--red)}
.pustrip{display:flex;gap:8px;padding:14px 20px 18px;overflow-x:auto}
.pu{flex:0 0 auto;min-width:132px;border:1px solid var(--line);border-radius:8px;
  padding:8px 10px 10px;background:var(--card,transparent)}
.pu-n{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--muted);
  letter-spacing:.04em;margin-bottom:4px}
.pu select{width:100%;font-size:14px;color:var(--ink);background:transparent;
  border:0;border-bottom:1px solid var(--line-strong);padding:3px 1px;cursor:pointer}
.pu select:focus{outline:0;border-bottom:2px solid var(--red)}
/* only one lamp is legal here - show it, but do not pretend it is a choice */
.pu-fixed{opacity:.72} .pu-fixed select{cursor:default;border-bottom-style:dotted}
.drybad{margin:0 20px 16px;padding:10px 12px;border-radius:8px;font-size:13.5px;
  color:var(--red);border:1px solid var(--red)}
@media (max-width:640px){ .pustrip{padding-left:16px;padding-right:16px} }

.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(196px,1fr));gap:18px 24px;padding:20px}
.fld{display:flex;flex-direction:column;gap:5px;min-width:0}
.fld input,.fld select{font-size:16px;color:var(--ink);background:transparent;border:0;
  border-bottom:1px solid var(--line-strong);border-radius:0;padding:5px 1px}
.fld select{cursor:pointer}
.fld input:focus,.fld select:focus{outline:0;border-bottom:2px solid var(--red)}
.fld input:read-only{color:var(--dim)}
.fld .hint{font-size:12px;color:var(--dim)}
.fld .chip{align-self:flex-start}

/* ---------- the machine, on the QUOTE (Tab 1 and Tab 3) ----------
   ONE component, rendered into every [data-mach] container, reading and writing the same
   state. Amit: "on tab 1 and tab 3" - tab 1 because the machine is named on the cover,
   tab 3 because it drives every number and you shouldn't have to leave the prices to
   change colours. It is deliberately NOT inside #sheet: renderSheet replaces that
   innerHTML on every view, which would rebuild these controls under the agent's cursor. */
.machbar{display:flex;align-items:flex-end;gap:26px;flex-wrap:wrap;padding:18px 20px}
.machbar .fld{min-width:118px}
.machbar .fld.narrow{min-width:0;width:78px}
.machbar .mlbl{font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--dim);align-self:center;margin-right:2px}

/* ---------- a fact the app owns, not a field ----------
   The offer number, the agent's name and the revision are the SYSTEM's. Rendering them
   as disabled inputs would read as broken ("why can't I type here?"), so they are text,
   on a dashed rule that says "this line is not yours to fill". */
.fact{display:flex;align-items:center;gap:7px;min-height:32px;font-size:16px;color:var(--ink);
  padding:5px 1px;border-bottom:1px dashed var(--line-strong)}
.fact .lock{flex:0 0 auto;font-size:11px;color:var(--dim);line-height:1}
.fact.await{color:var(--dim)}
.fact .num{font-family:var(--mono)}
/* THE CONTACT'S PHONE, AS SOMETHING YOU TAP. factContact() in crm.js; the second line of the
   Contact fact, under the name.

   MONO because it is a number and this stylesheet already sets numbers in mono (.fact .num,
   the grid's money and offer columns) - and because mono is what makes a column of numbers
   comparable at a glance. --red because it is the ONE thing on this card you act on rather
   than read, and it is the only link in the rail; no underline until hover, so the rail does
   not read as a page of links.

   min-height 32px matches .fact, so the two lines of a Contact field stack on the same
   rhythm as every other fact - and it is also about the smallest target a thumb hits
   reliably, which is the whole point of the feature. */
.factel{display:inline-flex;align-items:center;min-height:32px;align-self:flex-start;
  font-family:var(--mono);font-size:15px;color:var(--red);text-decoration:none}
.factel:hover{text-decoration:underline}
/* A phone field that holds no dialable digits ("ask reception", an extension nobody typed a
   number for). It is still a FACT, so it prints - it just is not a link. */
.fact.sub{min-height:0;font-size:14px;color:var(--muted);border-bottom:0;padding-top:0}
aside{min-width:0;min-height:0}
.rail{display:flex;flex-direction:column;gap:16px;height:100%;overflow-y:auto;
  overscroll-behavior:contain;padding-bottom:24px}
.card{padding:18px}
.card.big{border-top:3px solid var(--red)}
.total-v{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-top:8px}
.total-v .v{font-family:var(--mono);font-weight:700;font-size:30px;letter-spacing:-.02em}
.approx{font-size:13px;color:var(--dim);text-align:right;margin-top:3px}
.mini{display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--muted);padding:4px 0}
.mini .k{display:flex;align-items:center;gap:5px;min-width:0}
.mini .k em{font-style:normal;font-size:12px;color:var(--dim)}
.rule{height:1px;background:var(--line);margin:13px 0}
/* ISSUE IS BLACK, NOT RED, and that is about red meaning something rather than about taste.
   Red is Multitec's brand colour and stays exactly where it is - the header stripe and the
   wordmark. But it was ALSO the primary action, ALSO overdue, ALSO the hot stage, ALSO the
   chart maximum: when one colour means five things it means none, and the place that suffers
   most is the one that genuinely needs the alarm.
   So the actions go to ink - which is what "New quote" on the quotes grid already was, so the
   two primary buttons in the app now agree with each other instead of disagreeing. Red is left
   to the brand, and to the two things that are actually urgent: overdue, and hot. */
button.go{width:100%;border:0;border-radius:4px;padding:14px;cursor:pointer;background:var(--ink);
  color:var(--on-solid);font-size:15px;font-weight:700}
button.go:hover{filter:brightness(1.06)}
/* THE TWO BUTTONS ARE NOT THE SAME ACT, AND MUST NOT LOOK IT. They were identical full-width
   solid red, stacked: one is reversible (a draft you can keep editing), the other assigns a
   serial and produces the document the customer holds. Nothing on screen said which was which,
   and they are 8px apart.
   So the draft becomes an outline button - still full width, still the same size and the same
   target, because it is not a lesser ACTION, only a lesser COMMITMENT. Weight carries the
   difference; position and size stay put so muscle memory does not break. */
/* The reason the Issue button is disabled - beside it, not ON it. */
/* The sentence that says the act cannot be taken back. Amber, not red: red is the brand and
   the primary action, and a warning in the same colour as the button you are about to press
   reads as decoration on the button rather than a caution about it. */
.dlgwarn{margin:14px 0 0;padding:11px 13px;border-radius:5px;background:var(--amber-wash-2);
  border:1px solid var(--amber-line);font-size:13px;line-height:1.55;color:var(--ink)}
.gowhy{margin:7px 0 0;font-size:12.5px;line-height:1.5;color:var(--amber-ink,var(--muted))}
.gowhy[hidden]{display:none}
button.go.quiet{background:transparent;color:var(--ink);
  border:1px solid var(--line-strong);font-weight:600}
button.go.quiet:hover{filter:none;border-color:var(--ink);background:var(--soft)}
button.go[disabled]{background:var(--soft-2);color:var(--dim);cursor:not-allowed;
  border:1px solid var(--line-strong)}
button.mini-btn{border:1px solid var(--line-strong);background:var(--bg);color:var(--muted);
  border-radius:4px;font-size:12.5px;font-weight:500;padding:4px 9px;cursor:pointer;margin-left:7px}
button.mini-btn:hover{border-color:var(--ink);color:var(--ink)}
.block{background:var(--amber-wash);border:1px solid var(--amber-line);border-radius:4px;padding:13px;
  font-size:14px;color:var(--amber-ink)}
.diff{font-size:14px;border-left:3px solid var(--amber);padding:3px 0 3px 12px;margin:12px 0}
.diff.ok{border-left-color:var(--green)}
.diff .o{color:var(--dim);text-decoration:line-through}
.diff .n{color:var(--ink)}
details summary{cursor:pointer;font-size:14px;color:var(--muted);padding:7px 0}
.hid{font-size:14px;color:var(--muted);padding:7px 0;border-bottom:1px solid var(--line)}
.hid b{color:var(--ink);font-weight:500}
.note{font-size:14px;color:var(--muted);line-height:1.6;max-width:72ch}
.foot{padding:18px 0 0}
.savemsg{font-size:13px;color:var(--green);margin-top:9px;line-height:1.5}
/* SEND-FOR-APPROVAL banner. One box, three moods, set by a modifier class the JS toggles:
   .wait (with Multitec), .rej (returned with a reason), .ok (approved). Sits above the
   Save/Issue buttons in the rail. */
.apprban{margin:2px 0 10px;padding:10px 12px;border-radius:9px;font-size:12.5px;
  line-height:1.5;border:1px solid var(--line);background:var(--soft);color:var(--ink)}
.apprban[hidden]{display:none}
.apprban b{font-weight:600}
.apprban.wait{background:var(--amber-wash,var(--amber-wash));border-color:var(--amber-line);color:var(--amber-ink,var(--amber-ink))}
.apprban.rej{background:var(--red-wash,var(--red-wash));border-color:var(--red-line);color:var(--red,var(--red))}
.apprban.ok{background:var(--green-wash);border-color:var(--green-line);color:var(--green)}
.apprban .note{display:block;margin-top:4px;font-style:italic}
/* The reason box on the peek's Return panel. Inherits the banner's red frame, so it reads as
   part of the act rather than as a stray field; `resize:vertical` because a reason is
   sometimes one line and sometimes five, and neither should need scrolling to write. */
.reasonbox{display:block;width:100%;margin-top:8px;padding:7px 9px;border-radius:7px;
  border:1px solid var(--red-line);background:var(--bg);color:var(--ink);
  font:inherit;font-size:12.5px;line-height:1.45;resize:vertical}
.reasonbox:focus{outline:2px solid var(--red);outline-offset:-1px}
/* the "?" help affordance (decided over hover: it announces itself, works on touch, and
   stays open to be re-read). A small circle by a label; click opens a popover. Must-know
   facts stay inline (the freight approval chip, the qty max); only the WHY hides here. */
.lblrow{display:flex;align-items:center;gap:5px}
.help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;
  flex:0 0 auto;padding:0;border:1px solid var(--line-strong);border-radius:50%;
  background:var(--bg);color:var(--dim);font:600 11px/1 var(--sans);cursor:pointer}
.help:hover,.help:focus-visible{border-color:var(--red);color:var(--red)}
.pop{position:fixed;z-index:60;max-width:300px;background:var(--ink-bg);color:#fff;
  border-radius:8px;padding:12px 14px;font-size:13.5px;line-height:1.5;
  box-shadow:0 14px 44px -10px rgba(var(--shade),.55)}
.pop b{display:block;font-family:var(--display);font-size:14px;letter-spacing:.02em;
  text-transform:uppercase;margin:0 0 5px;color:#fff}   /* .pop's ground is --ink-bg */
.pop p{margin:7px 0 0}
.pop .em{color:var(--red-soft);font-weight:600}
.chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;
  color:var(--amber);background:var(--amber-wash);border:1px solid var(--amber-line);border-radius:4px;
  padding:3px 8px;margin-top:8px;line-height:1.3}
.chip.ok{color:var(--green);background:var(--green-wash);border-color:var(--green-line)}
/* A demo/training row, shown only when the admin flips "Show training data" (Phase E). Muted,
   small, inline - a marker, never a shout - and it never appears on a real row. */
.chip.train{color:var(--dim);background:var(--soft-2);border-color:var(--line-strong);font-weight:600;
  text-transform:uppercase;letter-spacing:.04em;font-size:9.5px;padding:1px 5px;margin:0 0 0 6px;
  vertical-align:middle}
/* The Show-training toggle in App settings (Phase E). A plain checkbox + label row. */
.switchrow{display:inline-flex;align-items:center;gap:9px;cursor:pointer;font-weight:600;
  font-size:14px;color:var(--ink)}
.switchrow input{width:17px;height:17px;cursor:pointer;accent-color:var(--red)}
#freightRow input{font-family:var(--mono)}

/* ---------- the four tabs: they mirror the four parts of the PDF ----------
   TABS, not a wizard - switch in any order, nothing is gated behind a "next". */
.sibs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px;min-height:28px}
.sib{display:inline-flex;align-items:center;gap:8px;background:var(--bg);
  border:1px solid var(--line-strong);border-radius:999px;padding:5px 13px;font-size:13px;
  color:var(--muted)}
.sib[aria-current=true]{border-color:var(--ink);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink)}
.sib .n{font-family:var(--mono);font-size:11px;color:var(--dim)}
.sibs .hint{font-size:12.5px;color:var(--dim)}
.tabs{display:flex;gap:4px;background:var(--bg);border:1px solid var(--line);border-radius:6px;
  padding:5px;margin-bottom:18px}
.tabs button{flex:1;display:flex;align-items:baseline;justify-content:center;gap:8px;border:0;
  background:transparent;border-radius:4px;padding:10px 12px;cursor:pointer;color:var(--dim);
  font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.06em;text-transform:uppercase}
.tabs button:hover{background:var(--soft);color:var(--ink)}
.tabs button[aria-selected=true]{background:var(--ink);color:var(--on-solid)}
.tabs .n{font-family:var(--mono);font-size:11px;font-weight:400;color:var(--line-strong)}
.tabs button[aria-selected=true] .n{color:var(--dim)}
.tabs .dot{width:6px;height:6px;border-radius:50%;background:var(--amber);align-self:center}

/* ---------- Tab 2: the specification. A reading table, not a grid. ---------- */
.specrow{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr) 104px;gap:14px;
  align-items:baseline;padding:9px 20px;border-bottom:1px solid var(--line)}
.specrow:hover{background:var(--soft)}
.specrow .k{font-size:15px;color:var(--muted)}
.specrow .v{font-size:15px;color:var(--ink)}
.specrow .flag{text-align:right}
.specrow .flag .tag{margin:0 0 0 6px}
/* the editable affordance, borrowed wholesale from .pin: invisible until you go near it. */
.ed{border:1px solid transparent;border-radius:4px;padding:3px 7px;margin:-3px -7px;cursor:text;
  white-space:pre-wrap}
.ed:hover{border-color:var(--line-strong);background:var(--soft)}
.ed:focus{background:var(--bg)}
.ed.edited{background:var(--amber-wash);border-color:var(--amber-line)}
/* The editable item NAME on the pricing tab reuses .ed, but flows inline inside the line's
   text cell rather than filling a block the way a spec value or a clause body does. */
.itemname{display:inline;font:inherit;white-space:normal}

/* ---------- Tab 4: the clauses. ---------- */
.clause{padding:14px 20px;border-bottom:1px solid var(--line)}
.clause:hover{background:var(--soft)}
.clause .ch{display:flex;align-items:baseline;justify-content:space-between;gap:14px;margin:0 0 6px}
.clause h3{margin:0;font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--dim)}
.clause .body{font-size:15px;color:var(--muted);line-height:1.6;max-width:80ch}
.sig{padding:16px 20px;background:var(--soft);font-size:14px;color:var(--muted);line-height:1.75}
.sig .lbl{display:block;margin-bottom:6px}

/* ---------- the PDF mode menu ----------
   position:FIXED, not absolute, and placed from the button's rect - exactly what .pop
   does, and for the same reason: the rail scrolls (overflow-y:auto), and an absolutely
   positioned child of a scrolling box gets clipped by it. */
.pdfwrap{position:relative}
.menu{position:fixed;z-index:50;background:var(--bg);
  border:1px solid var(--line-strong);border-radius:6px;overflow:hidden;
  box-shadow:0 18px 44px -14px rgba(var(--shade),.42)}
.menu button{display:block;width:100%;text-align:left;border:0;background:var(--bg);cursor:pointer;
  padding:11px 13px;border-bottom:1px solid var(--line)}
.menu button:last-child{border-bottom:0}
.menu button:hover{background:var(--soft)}
.menu b{display:block;font-family:var(--display);font-weight:600;font-size:14px;letter-spacing:.05em;
  text-transform:uppercase;color:var(--ink)}
.menu span{display:block;font-size:12.5px;color:var(--dim);line-height:1.45;margin-top:2px}
.menu button[aria-checked=true] b::after{content:" · default";color:var(--dim);font-weight:400;
  text-transform:none;letter-spacing:0}

/* ================= CRM and Admin =================
   No new design: every rule below composes the vocabulary the quote form already owns
   (.sheet/.sec/.grid/.fld/.row/.card/.chip/.tag/.fact). The only genuinely new objects are
   the combobox and the dialog, and both borrow their mechanics wholesale from .pop/.menu -
   position:fixed, placed from the trigger's rect, because .docs and .rail both scroll and
   an absolutely positioned child of a scrolling box gets clipped by it. */

/* One-column pages. `main` is a 2-col grid (docs | rail); CRM, Team and Settings have no rail.
   .pane is .docs exactly - same rules, same reason: `main` stretches its row to the shell's
   height and the pane is what scrolls, so the header stays put. */
main.page{grid-template-columns:minmax(0,1fr)}
.pane{min-width:0;overflow-y:auto;overscroll-behavior:contain;padding-bottom:60px}
.pane.mid{max-width:920px;margin:0 auto;width:100%}

/* ================= THE MASTER-DETAIL PATTERN (main.md) =================
   Amit: "since the list of agents can grow, the opuurtunities can grow and such, opening it
   in the same heightwise column makes it look jarring and not like a real app, should we not
   open the selected card on the right (like notion or similar apps)? also remember thet
   people might use this on thier mobiles and tablets so the design must be reesponsive."

   ONE PATTERN AT THREE WIDTHS, NOT THREE DESIGNS. Same DOM throughout; only these rules
   change. It is used by CRM (companies -> the company) and by Team (firms -> the people), and
   anything else with a list and a thing must use it rather than invent a fourth idea.

   THE PROMISE, and the only reason the pattern is worth anything: THE LIST DOES NOT MOVE WHEN
   YOU SELECT. That is half CSS (the detail has its own column, so it cannot push the list
   around) and half JavaScript (crmPick/teamPick flip aria-current in place and never repaint
   the rows). Break either half and the pattern is just a two-column layout.

   NO NEW DESIGN LANGUAGE: the list is .rec rows and the detail is .card/.sec/.grid - both
   already the quote form's. The only new marks are the back button and the slide. */
main.md{grid-template-columns:minmax(280px,340px) minmax(0,1fr);gap:22px}
/* NO LIST, NO LIST COLUMN. When the list itself could not be read (a 403, a router that is not
   mounted) there is nothing to select, so master-detail is the wrong shape: keeping two columns
   strands the explanation in a 340px gutter beside an empty half-page, which reads as "broken"
   rather than as "here is why". One column, wide enough for a paragraph.
   Set by paintTeamProblem(), cleared by paintTeam(). */
main.md[data-problem="1"]{grid-template-columns:minmax(0,1fr)}
main.md[data-problem="1"] > .mddetail{display:none}
main.md[data-problem="1"] > .mdlist{max-width:920px;margin:0 auto;width:100%}

/* ---- desktop: both columns, always. The back button does not exist here. ---- */
.mdback{display:none}

/* ---- tablet: the same two columns, list narrower. NOT a different design - the list gives
        up width because the detail is what you are reading, and that is the only change. ---- */
@media(max-width:1040px){
  main.md{grid-template-columns:minmax(210px,270px) minmax(0,1fr);gap:16px}
}

/* ---- phone: list full width; the detail SLIDES IN OVER IT, with a back arrow.
        A fixed overlay rather than a swapped pane, deliberately: the list stays mounted
        underneath, so coming back is instant, keeps its scroll position, and re-fetches
        nothing. It is the same "the list does not move" promise, honoured on a 390px screen
        where the two columns cannot both be on stage. ---- */
@media(max-width:720px){
  main.md{grid-template-columns:1fr}
  main.md > .mddetail{position:fixed;inset:0;z-index:40;background:var(--soft);
    padding:0 16px 40px;overflow-y:auto;overscroll-behavior:contain;
    transform:translateX(100%);transition:transform .22s ease;
    /* visibility, not display: a transition needs both states to be laid out, and
       visibility:hidden still keeps it out of the tab order while it is off stage. */
    visibility:hidden}
  main.md[data-open="1"] > .mddetail{transform:none;visibility:visible}
  body.mdopen{overflow:hidden}
  /* The back button is the overlay's own header, and it stays put while the detail scrolls -
     on a phone the way out must never be somewhere you have to scroll back up to find. */
  .mdback{display:flex;align-items:center;gap:8px;position:sticky;top:0;z-index:1;
    width:calc(100% + 32px);margin:0 -16px;padding:13px 16px;
    border:0;border-bottom:1px solid var(--line);background:var(--bg);cursor:pointer;
    font-family:var(--sans);font-size:15px;font-weight:500;color:var(--muted);text-align:left}
  .mdback::before{content:"←";font-size:17px;line-height:1;color:var(--red);font-weight:700}
  .mdback:hover{color:var(--ink)}
  /* The detail's empty .pagehead is a SPACER: on a desktop it lines the first card up with the
     list's "Contacts" heading. Inside the overlay there is no heading to line up with, so it
     is 44px of nothing directly under the back button. */
  main.md > .mddetail .pagehead:empty{display:none}
  main.md > .mddetail > #crmDetailBody,
  main.md > .mddetail > #teamDetailBody{padding-top:16px}
}
/* The app-shell scroll model lets go at tablet width: below this, html/body scroll and the
   panes simply grow. Both this and the phone overlay's `overflow-y:auto` match at 390px;
   `main.md > .mddetail` out-specifies a bare `.pane`, which is what keeps the overlay its own
   scroller. Specificity, not source order - so moving these blocks cannot break it. */
@media(max-width:1040px){
  .pane{overflow:visible}
}
/* ---------- the CRM's scope switch: WHOSE pipeline am I looking at? ----------
   Amit: "will i see the agents opuurtunities, etc as admin of multitec or as the superadmin
   (app admin)?" He is both, so he picks - and Multitec, his day job, is the default.

   It is a .seg, the same control the quote form uses for Export/Domestic - because it is the
   same KIND of question (two states, one true) and this app does not need a second way of
   asking it. It is drawn only for a session that can actually widen, and that is COSMETIC:
   db.agents.visibility() decides, and a narrowed answer repaints this switch rather than the
   switch deciding anything. See .scopenote for what happens when the server says no. */
.pagehead .seg{flex:0 0 auto}
.scopenote{font-size:12.5px;color:var(--dim);line-height:1.5;margin:8px 0 0}
.scopenote b{color:var(--muted);font-weight:600}
/* the server refused to widen - said out loud, because a switch that silently snaps back is
   a switch the user will keep pressing */
.scopenote.narrowed{color:var(--amber)}

/* the page's own opening sentence - what this screen is FOR, in its own words */
.lede{font-size:14.5px;color:var(--muted);line-height:1.6;max-width:74ch;margin:0}
.lede b{color:var(--ink);font-weight:500}
.pagehead{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;
  flex-wrap:wrap;margin:0 0 16px}
.pagehead h1{margin:0;font-family:var(--display);font-weight:600;font-size:26px;
  letter-spacing:.03em;text-transform:uppercase;color:var(--ink)}

/* ---------- honest degradation ----------
   A fetch that 404s (router not mounted) or 503s (no service key) must say SO. An empty
   list here would read as "you have no contacts", which is a lie the user cannot detect. */
.offline{padding:26px 22px;text-align:left}
.offline h3{margin:0 0 8px;font-family:var(--display);font-weight:600;font-size:17px;
  letter-spacing:.05em;text-transform:uppercase;color:var(--amber)}
.offline p{margin:0 0 8px;font-size:14.5px;color:var(--muted);line-height:1.6;max-width:70ch}
.offline code,.offline pre{font-family:var(--mono);font-size:12.5px}
.offline pre{margin:10px 0 0;padding:11px 13px;background:var(--soft);border:1px solid var(--line);
  border-radius:4px;overflow-x:auto;color:var(--muted);white-space:pre-wrap}
.empty{padding:22px;font-size:14.5px;color:var(--dim);line-height:1.6}
.spin{padding:22px;font-size:14px;color:var(--dim)}

/* ---------- buttons: one scale, three weights ---------- */
button.btn{border:1px solid var(--line-strong);background:var(--bg);color:var(--ink);
  border-radius:4px;font-size:14px;font-weight:500;padding:8px 14px;cursor:pointer;
  font-family:var(--sans)}
button.btn:hover{border-color:var(--ink)}
button.btn.primary{background:var(--red-solid);border-color:var(--red-solid);
  color:var(--on-red-solid);font-weight:700}
button.btn.primary:hover{filter:brightness(1.06)}
button.btn.quiet{border-color:transparent;color:var(--muted);font-weight:400;
  text-decoration:underline;text-underline-offset:3px;padding:8px 4px}
button.btn.quiet:hover{border-color:transparent;color:var(--ink);background:transparent}
button.btn[disabled]{opacity:.45;cursor:not-allowed}
/* The contact-card button: a glyph AND its word, the rule every reach control in this app
   follows - an 18px picture is not self-explanatory and a label is cheaper than a mis-tap. */
/* min-height MATCHES the plain .btn beside it. A glyph is shorter than a line of text, so
   without this the card button sat 3px under Edit's baseline in the same row - the kind of
   mismatch you only see once it is pointed at, and then cannot stop seeing. */
button.btn.vcard{display:inline-flex;align-items:center;gap:7px;padding:8px 12px;
  min-height:36px;box-sizing:border-box}
button.btn.vcard svg{flex:none}
/* ON A PHONE THE WORD GOES AND THE GLYPH STAYS, and this is a measurement, not a preference.
   A person row is `[facts][.rmeta]`, and at 390px "Contact card" took .rmeta from 56px to
   200px of a 356px row - which squeezed the facts column until Call and WhatsApp WRAPPED onto
   a second line. `qa/ui.py --only reach` measures the gap between that pair and reads -62px
   when they wrap; it must be >= 8. Hiding the word puts it back to exactly 8.
   Amit's rule is why the reach pair wins the argument and not the card button: "actions need
   to be done quickly and vcf type is once in a while" (docs/RULES.md 7b). The button keeps its
   44px target, its title and its aria-label - only the word is gone. */
@media(max-width:720px){
  .rec button.btn.vcard .tx{display:none}
  .rec button.btn.vcard{padding:8px 10px}
}
button.btn.working{background:var(--soft);border-color:var(--line)}
button.btn.danger:hover{border-color:var(--red);color:var(--red)}
.btnrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}

/* ---------- a list of records ---------- */
.rec{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:center;
  padding:13px 20px;border-bottom:1px solid var(--line);width:100%;text-align:left;
  background:var(--bg);border-left:0;border-right:0;border-top:0;font-family:var(--sans);cursor:pointer}
.rec:hover{background:var(--soft)}
.rec[aria-current=true]{background:var(--soft-2)}
.rec[aria-current=true] .rn{color:var(--red)}
.rec.off .rn,.rec.off .rs{color:var(--dim)}
/* A ROW THAT IS NOT ITSELF A CONTROL. The people roster used to be one big <button> that
   opened the editor, which meant a tel: link inside it both dialled AND opened the editor -
   and nested a link inside a button, which is invalid HTML. Now the row holds three separate
   targets (email, phone, Edit) and is a plain div, so it must stop LOOKING pressable: no
   pointer, no hover fill. The hairline and the padding stay - it is still a row. */
.rec.static{cursor:default}
.rec.static:hover{background:var(--bg)}
.rn{font-size:15.5px;font-weight:500;color:var(--ink);line-height:1.4;
  overflow-wrap:anywhere}
.rs{display:block;font-size:13px;color:var(--dim);margin-top:2px;line-height:1.45;
  overflow-wrap:anywhere}
.rmeta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.rnum{font-family:var(--mono);font-size:13px;color:var(--muted)}
/* a quiet status pill, in the .tag family but not shouting */
.pill{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.04em;
  text-transform:uppercase;padding:2px 7px;border-radius:3px;border:1px solid var(--line-strong);
  color:var(--muted);background:var(--bg);white-space:nowrap}
.pill.on{color:var(--green);background:var(--green-wash);border-color:var(--green-line)}
.pill.off{color:var(--dim);background:var(--soft);border-color:var(--line-strong)}
.pill.dom{color:var(--ink);background:var(--soft-2);border-color:var(--line-strong)}
/* A pill is a STATUS everywhere else (Active / Revoked / US$). An action must not borrow
   that shape and shout in the same voice. */
.pill.act{text-transform:none;font-weight:500;color:var(--dim);letter-spacing:0}
/* WHERE A DRAFT HAS GOT TO IN THE APPROVAL WORKFLOW, on the card's quote list. The same three
   hues the grid's .pchip uses for the same three words, so "draft · approved" is one colour
   wherever it is read. Green here and green on `on` (issued) is not a collision: the WORD is
   what tells them apart ("DRAFT · APPROVED" is not "ISSUED"), and both mean cleared. */
.pill.wf-submitted{color:var(--blue);background:var(--blue-wash);border-color:var(--blue-line)}
.pill.wf-approved{color:var(--green);background:var(--green-wash);border-color:var(--green-line)}
.pill.wf-rejected{color:var(--red);background:var(--red-wash);border-color:var(--red-line)}
/* SET IN SENTENCE CASE, and that is not a style preference - it is what makes the two-word
   pill FIT. This list is `.rec`'s grid (minmax(0,1fr) auto), so a wider pill takes the space
   from the offer number: measured in the browser, "DRAFT · APPROVED" is 124px and wraps
   "030826-1 rev1" onto a second line, while "draft · approved" is 101px and does not.
   It also spells the state exactly as the quotes grid spells it, which is the point of having
   one word (db/quotes.py:status_label) at all. */
.pill.wf-submitted,.pill.wf-approved,.pill.wf-rejected{text-transform:none;letter-spacing:0}

/* ---------- the agent chip: WHOSE BOOK this customer is in ----------
   Amit: "for the admin put a chip on the contact card to see who this one belongs to" /
   "also colourcode the chips so that i can see it from far".

   IDENTITY, NOT STATUS - and that distinction is the whole design:
     * A .pill is a STATUS (Active / Revoked / US$): outlined, light, quiet.
     * A .achip is an IDENTITY: solid fill, white text, readable across a room. No border to
       compete with the row it sits on, no gradient.
   NO RED, deliberately. Red means edited / pending / attention everywhere else in this app;
   an agent chip is not an alarm, and two unrelated things must not shout the same way. The
   palette below is therefore blues, purples, teals, olive, ochre and plum - chosen to stay
   apart from each other AND from --red / --green / --amber, all of which already mean
   something here. Every one clears 4.5:1 on white text.
   The HOUSE gets ink, not a hue: Multitec is not one of the agents, it IS the shop - the same
   reasoning as .tag.special, which is already ink for the same reason. */
.achip{display:inline-flex;align-items:center;gap:5px;border:0;border-radius:3px;
  padding:3px 8px;color:var(--on-solid);font-size:11px;font-weight:600;letter-spacing:.04em;
  text-transform:uppercase;white-space:nowrap;line-height:1.35;max-width:170px}
.achip .an{overflow:hidden;text-overflow:ellipsis}
.achip.house{background:var(--ink-bg);color:#fff}

/* ---------- search ----------
   No icon: this app has no icon vocabulary anywhere (the "?" help is a bordered circle of
   text, the only glyph is a padlock in one chip). A borrowed magnifier would be the one
   thing on the page pretending to be from a different design. type=search + the placeholder
   already say what it is. */
.search{padding:14px 16px;border-bottom:1px solid var(--line);background:var(--bg)}
.search input{width:100%;font-size:15px;color:var(--ink);background:var(--soft);
  border:1px solid var(--line-strong);border-radius:6px;padding:9px 12px}
.search input:focus{outline:0;border-color:var(--red);background:var(--bg)}
.search .hint{display:block;font-size:12px;color:var(--dim);margin-top:7px}

/* ---------- the combobox (the quote's customer / attention picker) ----------
   FIXED, placed from the input's rect - same reason as .menu: .docs scrolls. */
.combo{position:relative}
.combo input{width:100%;font-size:16px;color:var(--ink);background:transparent;border:0;
  border-bottom:1px solid var(--line-strong);border-radius:0;padding:5px 1px}
.combo input:focus{outline:0;border-bottom:2px solid var(--red)}
/* ABOVE THE SCRIM (70), NOT BELOW IT. This was 55, and it was invisible the day a combobox
   first lived inside a dialog: ＋ New opportunity's Person picker painted its list, correctly,
   UNDERNEATH the modal overlay. The field said "1 on this company" and typing did nothing you
   could see - Amit: "now i cant pick a person."
   It had never mattered because the combo had only ever been used on the quote form, where
   there is no scrim. The list is a child of <body> (it must be - .docs scrolls and would clip
   it), so it cannot inherit a stacking context from the dialog and has to out-rank it here.
   Safe in the other direction: openDlg() calls closeCombo() first, so a combo belonging to the
   page behind a modal can never be left floating over it. */
.colist{position:fixed;z-index:80;background:var(--bg);border:1px solid var(--line-strong);
  border-radius:6px;overflow:hidden auto;max-height:296px;
  box-shadow:0 18px 44px -14px rgba(var(--shade),.42)}
.colist .co{display:block;width:100%;text-align:left;border:0;background:var(--bg);
  cursor:pointer;padding:9px 13px;border-bottom:1px solid var(--line);font-family:var(--sans)}
.colist .co:last-child{border-bottom:0}
.colist .co:hover,.colist .co.cur{background:var(--soft)}
.colist .co b{display:block;font-size:14.5px;font-weight:500;color:var(--ink);line-height:1.4}
.colist .co span{display:block;font-size:12.5px;color:var(--dim);margin-top:1px}
.colist .co.mk b{color:var(--red);font-weight:600}
.colist .note{padding:10px 13px;font-size:13px;color:var(--dim);border-bottom:1px solid var(--line)}
/* the picked state: a fact the CRM owns, not a box to retype - same reasoning as .fact */
.picked{display:flex;align-items:center;gap:9px;min-height:32px;padding:5px 1px;
  border-bottom:1px dashed var(--line-strong)}
.picked .pn{flex:1 1 auto;min-width:0;font-size:16px;color:var(--ink);overflow-wrap:anywhere}
.fld .hint.from{color:var(--green)}

/* ---------- the dialog: creating a company or a person ----------
   ONE creation path, shared by the CRM page and the quote's picker. A second, sloppier one
   on the quote is exactly how the list gets dirty. */
.scrim{position:fixed;inset:0;z-index:70;background:rgba(var(--shade),.45);display:flex;
  align-items:flex-start;justify-content:center;padding:44px 20px;overflow-y:auto}
.dlg{background:var(--bg);border:1px solid var(--line);border-radius:8px;width:100%;
  max-width:580px;box-shadow:0 30px 80px -18px rgba(var(--shade),.5);overflow:hidden}
.foot2{display:flex;justify-content:space-between;align-items:center;gap:12px;
  flex-wrap:wrap;padding:14px 20px;background:var(--soft);border-top:1px solid var(--line)}
.foot2 .lbl{max-width:56ch;line-height:1.5}
.err{margin:0 20px 14px;padding:11px 13px;background:var(--red-wash);
  border:1px solid var(--red-line);border-radius:4px;font-size:13.5px;color:var(--red-ink)}

/* ---------- THE NEAR-MATCH PANEL ----------
   Amit: "this can create duplicate contacts with just one spelling or somma here and there
   and we will get a dirty contacts list in the future." He is right, and this panel is where
   that is won or lost. So: the matches come FIRST, above the fields, each one PICKABLE with
   a real button; and "create it anyway" is a quiet underlined link at the far end of the
   footer. The default path is to pick what already exists. */
.near{border-bottom:1px solid var(--line);background:var(--amber-wash)}
.near .nh{padding:13px 20px 4px}
.near .nt{font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.05em;
  text-transform:uppercase;color:var(--amber);margin:0}
.near .nw{font-size:13.5px;color:var(--amber-ink);line-height:1.55;margin:4px 0 0;max-width:64ch}
.near ul{list-style:none;margin:0;padding:8px 20px 16px}
.near li{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;
  background:var(--bg);border:1px solid var(--amber-line);border-radius:5px;padding:10px 12px;
  margin-bottom:7px}
.near li:last-child{margin-bottom:0}
.near li b{display:block;font-size:14.5px;font-weight:500;color:var(--ink);line-height:1.4;
  overflow-wrap:anywhere}
.near li span{display:block;font-size:12.5px;color:var(--dim);margin-top:1px}
.near li .nm{display:flex;gap:5px;margin-top:5px}
.near li .nm .pill{display:inline-block}
.near.checking{background:var(--soft)}
.near.checking .nt{color:var(--dim)}

/* ---------- PRESENCE: a colleague is on this firm ----------
   Amit: "whats a better way of saying that he is out of his reach and still keep it gated - so
   that he doesnt duplicate quote or make dulpicate efforts." His rule:

       You can always see THAT a colleague is on a customer you can see.
       You can never see WHAT he offered.
       Presence is not price.

   ONE LOOK, THREE PLACES. The band on the company card and the speed bump in the quote form
   are the SAME .pband, because they are the same fact met on two roads - and the chip inside
   both is the CRM's own .achip with the CRM's own hue, so "who is this person" is one visual
   answer everywhere it is asked. (crm.js: presenceChip() IS ownerChip(), with one sentence
   swapped.)

   AMBER, NOT RED, and not a padlock. Red means "attention/edited/pending" everywhere in this
   app and this is not an alarm - Martin has done nothing wrong. .near already owns exactly this
   register: "stop for a second, look at what already exists". This is the same sentence about a
   colleague instead of a duplicate record, so it is deliberately the same wash rather than a
   fourth colour. It is NOT 403-flavoured: within a team this is OWNERSHIP, not permission.

   AND THE MONEY IS NOT HERE, IN ANY FORM. No greyed figure, no "₹••••", no blur. A blurred
   number says one exists and teases with it, and it reads as a broken permission - which is
   precisely what this is not. Whose it is, in words, and nothing else. There is no CSS below
   for a number because there is no number.

   .free is the OTHER half, and the load-bearing one: a firm nobody is on says so. Green, the
   .enrich green - it is the only "go" in this app's vocabulary, and "yours to take" is a go. */
.pband{grid-column:1/-1;background:var(--amber-wash);border:1px solid var(--amber-line);
  border-radius:5px;padding:12px 14px;margin-bottom:16px}
.pband .nt{font-family:var(--display);font-weight:600;font-size:15px;letter-spacing:.05em;
  text-transform:uppercase;color:var(--amber);margin:0;line-height:1.35;overflow-wrap:anywhere}
.pband .nw{font-size:13.5px;color:var(--amber-ink);line-height:1.55;margin:8px 0 0;max-width:64ch}
.pband .nw b{color:var(--ink);font-weight:600}
.pband .prow{list-style:none;margin:9px 0 0;padding:0}
/* wrap, never a row that scrolls: two colleagues on one firm is rare, three is rarer, and at
   390px the chip and its meta must stack rather than push the card sideways. */
.pband .prow li{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:5px}
.pband .prow li:last-child{margin-bottom:0}
.pband .pm{font-size:12.5px;color:var(--amber-ink)}
.pband .pph{font-size:12.5px;color:var(--amber);font-weight:600;font-family:var(--mono);
  text-decoration:none;border-bottom:1px solid var(--amber-line)}
.pband .pph:hover{color:var(--ink);border-bottom-color:var(--ink)}
/* IN THE QUOTE FORM. .grid's own gap already spaces it, so the card's bottom margin would
   double up into a hole between Customer and Address. */
.pband.inline{margin-bottom:0}
.pband.free{background:var(--green-wash);border-color:var(--green-line)}
.pband.free .nt{color:var(--green)}
.pband.free .nw{color:var(--green-ink);margin-top:4px}
/* the grid's "Also on" cell: chips, wrapping, the same rule .ochips:empty already keeps */
.pres{display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0}
.pres:empty{display:none}

/* ---------- enrichment: a SUGGESTION, never a write ---------- */
.enrich{grid-column:1/-1;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  padding:11px 13px;background:var(--green-wash);border:1px solid var(--green-line);border-radius:4px}
.enrich .et{flex:1 1 220px;min-width:0;font-size:13.5px;color:var(--green-ink);line-height:1.5}
.enrich .et b{color:var(--ink);font-weight:600}
.enrich button.btn{padding:5px 11px;font-size:13px}

/* ---------- Admin ---------- */
.kv{display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px 14px;font-size:14px;
  color:var(--muted);margin:12px 0 0}
.kv dt{color:var(--dim);font-size:12.5px}
.kv dd{margin:0;font-family:var(--mono);color:var(--ink)}
.serial-now{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:2px 0 0}
.serial-now .v{font-family:var(--mono);font-weight:700;font-size:26px;color:var(--ink);
  letter-spacing:-.01em}
.willbe{font-size:13.5px;line-height:1.6;color:var(--amber-ink);background:var(--amber-wash);
  border:1px solid var(--amber-line);border-radius:4px;padding:11px 13px;margin-top:14px}
.willbe b{font-family:var(--mono);color:var(--ink)}
/* the agent row's second line: email · the label this firm prints on a cover */
.rs .sep{color:var(--line-strong)}
.rs b{color:var(--muted);font-weight:600}

/* ---------- a phone number you can ring, wherever it appears ----------
   shell.js's telHtml(). The default class for a number sitting inside a line of other
   details - a roster row's "Director · j@acme.com · +91 98765 43210", a duplicate-candidate
   subtitle. Two richer variants exist and keep their own rules: .factel (the opportunity
   rail, where the number gets its own line) and .pband .pph (the presence band's amber).

   MONO, because it is a number and this stylesheet sets numbers in mono everywhere. It does
   NOT take --red: these numbers sit inside dim subtitles, and a row of red phone numbers
   down a roster would out-shout the names they belong to. Inheriting the subtitle's colour
   and going to --ink on hover is enough to say "this one is live" without turning the list
   into a page of links.

   min-height 24px: smaller than the rail's 32px because it is inline in a line of text, but
   still a target a thumb can find. */
.tel{font-family:var(--mono);color:inherit;text-decoration:none;
  border-bottom:1px dotted var(--line-strong);display:inline-block;min-height:24px}
.tel:hover{color:var(--ink);border-bottom-color:var(--ink)}
/* THE EMAIL, ITS TWIN. Same dotted underline and the same "live, but not shouting" weight,
   because reaching a person by mail and reaching them by phone are the same kind of act. It
   keeps the page's SANS face where .tel takes mono: an address is words, and setting words
   in mono makes them read as a code you have to decipher rather than a name you recognise.
   `overflow-wrap:anywhere` because a long address must wrap inside a 300px rail, not push
   the row wide. */
.mail{font-family:var(--sans);color:inherit;text-decoration:none;
  border-bottom:1px dotted var(--line-strong);display:inline-block;min-height:24px;
  overflow-wrap:anywhere}
.mail:hover{color:var(--ink);border-bottom-color:var(--ink)}

/* ---------- REACHING A PERSON: two buttons, one action each ----------
   Amit: "get the whatsapp icon and tel icon then the user can choose. or is a glyph too small
   for fat fingers?"

   THE GLYPH IS 18px AND THE TARGET IS 44px, which is the answer to his question: those are two
   different measurements and only the second decides whether a thumb lands. 44 is Apple's
   minimum (Android asks 48dp, and the 52px this comes out at clears both); the 8px gap is
   there so the pair cannot be hit as one. The word under each picture is not decoration - the
   WhatsApp mark is recognised, an 18px handset is not, and a label costs less than a mis-tap.

   WHY THE NUMBER BESIDE THEM IS PLAIN TEXT wherever this block is drawn: a glyph next to a
   number that is ITSELF a tel: link would be two targets of different sizes doing different
   things, one of them invisible - the shape of the bug already caught once on the company card
   ("it does click on tel: but also opens the edit pane"). Every other number in the app keeps
   its .tel link and gets .telmenu instead, which needs no width. */
.reach{display:flex;gap:8px;margin-top:6px;flex-wrap:wrap}
.reachb{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
  gap:3px;min-width:62px;min-height:44px;padding:6px 9px;border:1px solid var(--line-strong);
  border-radius:7px;background:var(--bg);color:var(--muted);text-decoration:none;
  font:500 11px/1.2 var(--sans);cursor:pointer;-webkit-tap-highlight-color:transparent}
.reachb svg{width:18px;height:18px;display:block;flex:0 0 auto}
.reachb:focus-visible{outline:2px solid var(--red-bright);outline-offset:1px}
/* WhatsApp's own green, and ONLY on hover. At rest both buttons are the page's grey so the
   pair reads as one control with two halves; brand colour at rest would make the WhatsApp
   half look like the recommended one, and which of the two is right is the customer's fact,
   not ours. */
.reachb.wa:hover{border-color:var(--wa);color:var(--wa-ink);background:var(--wa-wash)}
/* AND CALL LIGHTS UP JUST AS DEFINITELY. Amit: "the tel icon or glyph does not light up like
   the whatsapp one too."

   He is describing an imbalance, not a missing rule: Call DID have a hover, but it was the
   generic one - a hairline border and the faintest wash - sitting next to a button that turns
   green. Two halves of one control, one of which looks alive. A pair where only one half
   responds reads as the other half being the real button, which is the same fault as the
   right-click menu that nobody found: the affordance was there and it was not legible.

   INK, NOT A NEW HUE, and that is the app's own answer rather than a taste: timeline.js's
   TL_FAMILY files a call under `say`, whose colour is ink - "four families, not twelve
   colours". Red is out for the reason recorded there ("red means attention everywhere else in
   this app, and a logged call is not an alarm"), and green belongs to WhatsApp. So Call gets
   the strongest NEUTRAL - a full ink border and the raised surface - which lands with the same
   weight as the green without inventing a fifth colour to memorise. */
.reachb:hover{border-color:var(--ink);color:var(--ink);background:var(--soft-2)}
/* NO COUNTRY CODE, NO WHATSAPP - said out loud rather than hidden. wa.me takes a full
   international number and nothing else; a missing button would be a mystery, this is a
   to-do. `title` carries the fix. */
.reachb.off{opacity:.42;cursor:not-allowed}
.reachb.off:hover{border-color:var(--line);color:var(--muted);background:var(--bg)}

/* ---- the same pair in a TABLE CELL: square, wordless, still a target ----
   Amit, 2026-07-29, on the People grid: "please give me tel and wa buttons here also - or
   suggest some better alternative."

   THE WORD IS WHAT DOES NOT FIT, NOT THE BUTTON. 62px each is the LABEL's width; the glyph
   needs 18. Two squares and a 6px gap come to 70px, which a Phone column has beside a number
   and a labelled pair never did. The label is still said - `title` and `aria-label`, written
   by reachHtml - so nothing is lost but the pixels.

   32px IS A MOUSE TARGET AND NOT A THUMB, which is why the phone rule below exists rather than
   one size for both. A grid row is 37px tall; 44px squares on a desktop would make the table a
   third taller to serve a finger that is not there. Under 720px the finger IS there, the row
   grows, and that is the correct trade in that direction.

   nowrap ON THE CELL, and it is not tidiness - it is the whole difference between a table and a
   list. .ogc wraps by design (a long company name should take two lines rather than be cut),
   but a Phone cell that wraps puts the pair UNDER the number, and 37px rows become 75px: at
   1440 that is seven people on screen where there were twelve. The first build of this did
   exactly that, and every other check passed - both buttons were present, real and the right
   size, just in the wrong place. So the digits ellipsise (flex:0 0 auto keeps the squeeze on
   the TEXT, never on the targets) and the column's min-width, set in crm.js:pplCols, is what
   keeps the ellipsis from ever being needed. */
.ogc.phone{flex-wrap:nowrap}
.reach.mini{margin-top:0;gap:6px;flex-wrap:nowrap;flex:0 0 auto}
.reach.mini .reachb{width:32px;height:32px;min-width:0;min-height:0;padding:0;border-radius:6px}
.reach.mini .reachb svg{width:17px;height:17px}
@media(max-width:720px){
  .reach.mini .reachb{width:44px;height:44px}
  .reach.mini .reachb svg{width:19px;height:19px}
}

/* ---------- the same two actions on every OTHER number: right-click / long-press ----------
   Amit: "can we get on right click on the phone number an option to open in tel: or to open in
   whatsapp right from there?"

   position:FIXED and placed from the POINTER, for the reason .pop and .menu both document -
   the rails and panes scroll, and an absolutely positioned child of a scrolling box is clipped
   by it. z-index 90 puts it over everything the app can already have on screen: the peek (40),
   the gate (60), the column pickers (80). A menu that opens UNDER the dialog that produced it
   is the bug fixed on the person picker. */
.telmenu{position:fixed;z-index:90;min-width:210px;background:var(--bg);
  border:1px solid var(--line-strong);border-radius:7px;overflow:hidden;
  box-shadow:0 18px 44px -14px rgba(var(--shade),.42)}
.telmenu .tmnum{font-family:var(--mono);font-size:12.5px;color:var(--dim);
  padding:9px 12px 8px;border-bottom:1px solid var(--line);background:var(--soft)}
.telmenu a,.telmenu button,.telmenu .off{display:flex;align-items:center;gap:10px;width:100%;
  text-align:left;border:0;border-bottom:1px solid var(--line);background:var(--bg);
  color:var(--ink);text-decoration:none;cursor:pointer;padding:0 12px;min-height:44px;
  font:500 13.5px/1.4 var(--sans)}
.telmenu > :last-child{border-bottom:0}
.telmenu svg{width:17px;height:17px;flex:0 0 auto;color:var(--muted)}
.telmenu a:hover,.telmenu button:hover{background:var(--soft)}
.telmenu a:hover svg,.telmenu button:hover svg{color:var(--ink)}
.telmenu .off{color:var(--dim);cursor:default;font-size:12.5px}

/* ---------- Admin › the PEOPLE under a firm ----------
   Amit: "you have to have phone numbers for the people working for agents too (multiple
   sales people for same agent)... also assign one person to be the agent admin (like bruce
   who will look after all sales guys in ipex, me who will look after all sales guys in
   Multitec)".

   NO NEW DESIGN. This is .rec's own grid, indented and set on --soft, plus a radio: the
   people are a level BELOW the firm, and the page should look like the sentence "these are
   Ipex's salesmen" rather than like a second, unrelated table. The red left rule is the only
   new mark, and it is the tree line that says "these belong to the row above". */
/* A real BORDER, not an inset box-shadow: a child's own background (.pedit, .prow:hover)
   paints over an inset shadow and the tree line disappears exactly where the eye is. */
.people{background:var(--soft);border-bottom:1px solid var(--line);
  border-left:3px solid var(--red)}
.pnote{font-size:13.5px;color:var(--muted);line-height:1.6;padding:13px 20px 13px 24px;
  border-bottom:1px solid var(--line)}
.pnote b{color:var(--ink);font-weight:600}
.pnote .hl{color:var(--red);font-weight:600}
/* the cover, quoted: the exact two-part line a customer reads. Set like the document it
   comes from - the label quiet and condensed, the value in the figure face - so it reads as
   "here is the paper", not as another UI control. */
.cover-eg{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:9px 0;
  padding:9px 12px;background:var(--bg);border:1px solid var(--line);border-radius:4px}
.cover-eg .cl{font-family:var(--display);font-weight:600;font-size:12.5px;letter-spacing:.06em;
  text-transform:uppercase;color:var(--dim);white-space:nowrap}
.cover-eg .cv{font-size:13.5px;color:var(--ink)}
.prow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:14px;align-items:center;
  padding:11px 20px 11px 24px;border-bottom:1px solid var(--line)}
.prow:last-child{border-bottom:0}
.prow:hover{background:var(--bg)}
.prow.off .rn,.prow.off .rs{color:var(--dim)}
/* THE ONE ADMIN. A radio, because set_agent_admin() demotes the incumbent as it promotes -
   the control is shaped like the rule. It carries its own label: an unlabelled radio in a
   list of people is a question with no question. */
.padm{display:flex;align-items:center;gap:7px;cursor:pointer;white-space:nowrap}
.padm input[type=radio]{width:16px;height:16px;accent-color:var(--red);margin:0;cursor:pointer}
.padm .pl{font-size:12px;font-weight:500;color:var(--dim)}
.padm input:checked ~ .pl{color:var(--red);font-weight:600}
.padm.dis{cursor:not-allowed;opacity:.5}
.pedit{padding:14px 20px 14px 24px;border-bottom:1px solid var(--line);background:var(--bg)}
.pedit:last-child{border-bottom:0}
.pedit .grid{padding:0}
@media(max-width:720px){
  .prow{grid-template-columns:auto minmax(0,1fr);row-gap:8px}
  .prow .rmeta{grid-column:2/-1;justify-content:flex-start}
}

/* ================= THE PHONE =================
   Amit: "people might use this on thier mobiles and tablets so the design must be reesponsive
   web app - design towards this goal."

   THE ONE HARD RULE: NOTHING SCROLLS SIDEWAYS. A page that pans left and right is the single
   clearest tell that it was drawn for a desktop and shipped anyway. Everything here is that
   rule applied - and where a table genuinely cannot fit (the priced lines have five columns
   and every one is load-bearing), it gets its OWN horizontal scroller rather than pushing the
   page. One box scrolls sideways; the page never does.

   SAME COMPONENTS, SMALLER. No phone-only screens and no features dropped: an agent on a
   phone can do everything an agent on a laptop can do. */
@media(max-width:720px){
  /* The header keeps Amit's settled lockup ([logo] | Sales, beside, never under) untouched.
     What gives way is the NAV CENTRING - flex:1 on both sides is a desktop trick, and at
     390px the side columns have no width to donate, so the nav drops to its own full-width
     line instead of being defended into a corner. Four items still fit across. */
  .bar{flex-wrap:wrap;gap:8px 14px;padding:10px 16px 10px}
  .bar > .brand{flex:0 0 auto}
  /* On a phone the reserved desktop width would shove the toggle off-screen, so drop it and
     let the identity toggle WRAP its buttons instead of overflowing. */
  .bar > .sess{flex:1 1 auto;min-width:0;align-items:flex-end}
  #role{flex-wrap:wrap;justify-content:flex-end;width:auto}
  /* ---------- ON A PHONE THE NAV IS A DRAWER ----------
     Amit: "on mobile dont clutter, have it as a hamburger menu that opens on the side".
     Seven items across 390px was the clutter - the old rule below spread FIVE across the full
     width on their own row, and the flattened nav would have made that a scrum of half-words.

     A DRAWER, and specifically the app's own drawer: `data-open` + a scrim + Escape, the same
     three parts the quote peek and the Due drawer use. A phone has one way of putting a panel
     over a page here and this is it - a second idea would be a second thing to learn.

     FROM THE LEFT, because the hamburger that opens it is top-left and a panel should come
     from the edge you pressed. (The peek and the Due drawer come from the right; those are
     opened from the right.) It stops short of the full width so the page behind stays
     visible - a drawer you can see past is a drawer you know how to dismiss. */
  .navtoggle{display:inline-flex}
  /* AT THE LEFT EDGE, BEFORE THE LOGO. Amit, on his phone: "the hamburger menu on the ios is
     after the logo. not a good placement."

     He is describing a real thing rather than a preference. Sitting between the wordmark and
     the identity toggle, the button reads as PART OF THE BRAND LOCKUP - a chevron on the logo,
     something that expands Multitec - instead of as the way into the app. Every phone he uses
     puts this control hard against the leading edge, and a control in the wrong place is one
     nobody presses. CLAUDE.md already says it of a right-click menu; it is the same law.

     `order:-1` RATHER THAN MOVING THE MARKUP, and that is the surgical choice: the button
     must stay next to the <nav> it labels for a screen reader (aria-controls points at it) and
     must keep its desktop position, where it is `display:none` and has none. One line, one
     breakpoint, nothing else in the header disturbed - the lockup itself ([logo] | Sales,
     beside, never under) is untouched, it simply starts 46px further in.

     THE DRAWER STILL COMES FROM THE LEFT, which is now more true than it was: the panel opens
     from the exact edge the thumb pressed. */
  .navtoggle{order:-1}
  /* "Signed in as" + its "?" cost a whole line of a 390px header to caption a control that
     already says a name and wears the due count. The button IS the answer to the question the
     label asks; on a phone the label is the first thing to go. */
  .bar > .sess .lblrow{display:none}
  .bar > .sess{min-width:0}
  .nav{position:fixed;top:0;left:0;bottom:0;z-index:60;width:min(280px,82vw);
    display:flex;flex-direction:column;align-items:stretch;gap:2px;
    background:var(--bg);border-right:1px solid var(--line);padding:14px 12px;
    transform:translateX(-101%);visibility:hidden;transition:transform .18s ease;
    box-shadow:0 0 40px rgba(var(--shade),.14);overflow-y:auto;overscroll-behavior:contain}
  .nav[data-open="1"]{transform:none;visibility:visible}
  /* Full-width rows in the drawer: a nav item is a destination here, not a tab, so it gets
     the whole line and a real target height rather than the 9px padding a row needed. */
  .nav button{padding:11px 12px;font-size:15px;text-align:left;border-radius:7px}
  .navscrim{display:block;position:fixed;inset:0;z-index:59;background:rgba(var(--shade),.28)}
  .navscrim[hidden]{display:none}
  .seg button{padding:6px 10px;font-size:13px}

  main{padding:14px 16px 0;gap:16px}
  .pagehead h1{font-size:22px}

  /* The 4 tabs wrap to two rows. NOT a horizontal scroller: that would hide Terms behind an
     edge, and Terms is where the PDF unlocks - the one place a hidden control has a real cost. */
  .tabs{flex-wrap:wrap;padding:4px;gap:2px}
  .tabs button{flex:1 1 40%;padding:9px 6px;font-size:13px;letter-spacing:.03em}

  /* THE PRICED LINES. Five columns, all of them wanted: tick, description, qty, price each,
     total. They cannot fit 390px, and reflowing them into pairs would turn a table you read
     down into a list you cannot compare. So the SHEET scrolls inside its own box - CLAUDE.md's
     own instruction for a wide table - and the page stays put.

     The figure columns are cut to what the numbers actually need rather than left at their
     desktop widths, because every pixel taken off them is a pixel the DESCRIPTION gets - and
     the description is the column you read. At the desktop's 112/124 the description fell to
     ~144px and "Base Machine Description" set as a three-line chimney. The total column still
     holds the widest real figure the PDF is sized for. */
  .sheet{overflow-x:auto}
  .cols,.row{min-width:512px;grid-template-columns:20px minmax(0,1fr) 46px 84px 96px;gap:10px}
  .tot,.pin{font-size:14px}
  .q{font-size:13px;padding:4px 2px}
  /* The description BLOCKS stay at page width and do NOT join the scroller. They are prose -
     the machine written out, the way it reads on the PDF - so making them 512px wide would
     mean scrolling sideways to read a sentence, which is the opposite of the point.
     Only the priced GRID scrolls; the words it describes stay put. */
  .blk{padding-left:16px;padding-right:16px}
  /* A vendor pick is a sentence with a dropdown after it. At this width the desktop's three
     columns squeeze the sentence into a chimney ("BST / Automatic / Web Guide / with..."), so
     the sentence takes the full width and the control drops under it. Nothing is lost: the
     delta is information, not a column anybody compares down. */
  .blk li.pick{grid-template-columns:minmax(0,1fr) auto;row-gap:8px;padding-right:0}
  .blk li.pick > span:first-child{grid-column:1/-1}
  .blk li.pick .picker{width:auto;max-width:100%;justify-self:start}
  .blk h3{grid-template-columns:minmax(0,1fr) auto}

  /* The specification is prose, not figures, so it CAN reflow where the price table cannot:
     label over value, full width, nothing to compare across columns. */
  .specrow{grid-template-columns:minmax(0,1fr) auto;row-gap:2px;padding:10px 16px}
  .specrow .k{grid-column:1/-1;font-size:13px}

  .sec{padding:14px 16px 10px;flex-wrap:wrap}
  .sec h2{font-size:18px}
  .grid{padding:16px;gap:14px 16px;grid-template-columns:1fr}
  .cols,.row{padding-left:16px;padding-right:16px}
  .clause,.pdfnote,.sig,.empty,.spin,.pnote{padding-left:16px;padding-right:16px}
  .foot2{padding:12px 16px}
  .err{margin-left:16px;margin-right:16px}
  .machbar{padding:14px 16px;gap:16px}

  /* ---------- THE RUNNING TOTAL IS A BOTTOM BAR ON A PHONE ----------
     SETTLED. This block used to be a ⚠️ OPEN note listing two options and refusing to guess
     between them; Amit picked: "yes, do the bottom bar".

     WHAT IT COST WHILE IT WAS OPEN: on the Pricing tab at 390px the total measured 9,586px
     down a 9,949px document. Eleven screens of accessories before the price appears, on the
     one promise the product is built around.

     AND IT TAKES NEITHER OPTION THE OLD NOTE OFFERED. It warned that a fixed bar would
     duplicate the figure "so two things to keep in step" - so nothing is duplicated: the SAME
     .card.big that is the desktop rail is what goes `position:fixed` here. One element, one
     number, one set of ids for quote.js to write to. `position:sticky` was the earlier failed
     attempt (it cannot leave .rail, its containing block); `fixed` has no containing block to
     be trapped in, which is exactly why it works where sticky silently did not.

     COLLAPSED BY DEFAULT to one line - label, figure, two buttons - because that is all you
     need while picking. The breakdown (Machine / Accessories / Freight / Discount) is one tap
     away and nothing is hidden that was not already below the fold. */
  .card.big{position:fixed;left:0;right:0;bottom:0;z-index:50;margin:0;border-radius:0;
    display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;
    background:var(--bg);border:0;border-top:3px solid var(--red);
    /* the iPhone's home indicator sits in the last ~34px - without this the buttons are
       under it and the bar reads as clipped */
    padding:9px 16px calc(9px + env(safe-area-inset-bottom, 0px));
    box-shadow:0 -6px 22px rgba(var(--shade),.13);max-height:78vh;overflow-y:auto}
  /* LINE 1 is the label and the figure; LINE 2 is the two buttons. The wrap is arranged by
     basis, not by a spacer: label+figure fill the first line, and 45%-basis buttons cannot
     then fit beside them, so they drop together and grow to half each. */
  .totmore{display:flex;align-items:center;gap:7px;flex:1 1 auto;order:1;
    border:0;background:transparent;padding:0;cursor:pointer;min-height:32px}
  .totchev{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
    border-bottom:5px solid var(--dim);transition:transform .15s ease}
  .card.big[data-open="0"] .totchev{transform:rotate(180deg)}
  .card.big .total-v{order:2;flex:0 0 auto;margin:0;white-space:nowrap}
  .card.big .total-v .v{font-size:22px}
  .card.big .approx{order:3;flex:1 1 100%}
  .card.big .rule,.card.big .mini,.card.big .chip{order:4;flex:1 1 100%}
  /* COLLAPSED: the breakdown goes, the headline and the actions stay. */
  .card.big[data-open="0"] .rule,
  .card.big[data-open="0"] .mini,
  .card.big[data-open="0"] .chip,
  .card.big[data-open="0"] .approx{display:none}
  /* The two buttons share the last line - side by side, because stacked they cost 100px of a
     844px screen for the whole session. `!important` beats the inline margin on .pdfwrap. */
  .card.big #save{order:5;flex:1 1 45%;min-width:0;padding:11px 8px;font-size:13.5px;
    white-space:nowrap}
  .card.big .pdfwrap{order:6;flex:1 1 45%;min-width:0;margin-top:0 !important}
  .card.big .pdfwrap .go{padding:11px 8px;font-size:13.5px;white-space:nowrap}
  .card.big .gowhy{order:7;flex:1 1 100%;margin:0}
  /* THE PAGE MUST NOT END UNDER THE BAR. ~72px collapsed; the expanded state scrolls itself
     rather than growing the page, so one value is enough. */
  main.page{padding-bottom:86px}
  .total-v .v{font-size:26px}
  .pop{max-width:calc(100vw - 24px)}
  .scrim{padding:16px 12px}
  .achip{max-width:120px}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}

/* ============================================================ REPORTS · how the selling went
   Amit: "i would also liek reports by model (S3S, S2, ...) by stage, by agent, by salesman
   and so on."

   NO CHART LIBRARY, and that is not a compromise. There is no bundler and no CDN (the app must
   not gain a build step), and a bar chart of six stages is a div with a width. Every charting
   library's default look is the most AI-looking thing available, and Amit rejects exactly that
   - "shabby ai work". So: the app's own faces, the app's own ink, and bars that are one colour.

   THE BARS ARE INK, NOT RED. --red means ATTENTION everywhere else in this app (a warning
   chip, a pending approval, the focus ring). Six red bars would shout six times and mean
   nothing - the same reasoning that keeps red out of the .achip palette. The bar is a
   comparison, not an alarm.

   Appended at the foot rather than woven in, so this block is one thing to read and one thing
   to delete. It reuses .sheet / .sec / .card / .achip / .pagehead / .scopenote wholesale - a
   Reports page that invented its own card would be a second design system on one screen. */
.rtiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:12px;
  margin:0 0 18px}
.rtile{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:14px 16px;
  display:flex;flex-direction:column;gap:3px;min-width:0}
.rtile .rv{font-size:26px;font-weight:600;line-height:1.15;color:var(--ink);
  letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis}
/* THE NATIVE SUBTOTALS MUST NOT BE CLIPPED. They wrap; they do not ellipsis.
   This was caught in a screenshot: three currencies read "€ 388,670 · ₹ 12,85,82,000 · US$…"
   and the dollars - the largest of the three - fell off the end. That is not a cosmetic
   truncation. These figures are the HONEST half of every converted total on this page (the
   numbers that were never multiplied by anything), and an ellipsis quietly deletes the
   evidence while leaving the converted figure above it looking authoritative. Let the tile
   grow instead: the grid stretches its row, so the others grow with it. */
.rtile .rsub{font-size:12px;color:var(--dim);white-space:normal;line-height:1.45;
  overflow-wrap:anywhere}
.rsheet + .rsheet{margin-top:18px}
/* THE PAGE MUST NEVER SCROLL SIDEWAYS. A wide table scrolls INSIDE its own container - on a
   390px phone `By agent` is genuinely wider than the screen and the honest answer is to let it
   scroll where it lives, not to drop columns (a column nobody can see is a fact nobody has)
   and not to push the whole page off-axis. */
.rwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
/* `width:auto` + `min-width:100%`, NOT `width:100%` - and this is the whole of the fix for
   the report that ran off the edge of Amit's phone. `.rwrap` above says the honest answer to
   a table wider than the screen is to let it scroll; `width:100%` then quietly contradicted
   it, because it forces the table to FIT the wrapper, and a table that must fit squeezes its
   columns instead of overflowing them. With `white-space:nowrap` on every cell the squeeze
   has nowhere to go, so `Won value (₹)` kept its width and `As the firm wrote it` collapsed
   to a one-character-per-line ribbon while Commission was still clipped off the right edge -
   a column nobody can see, which is the exact outcome the comment above rules out.
   `auto` + `min-width:100%` is equivalent when the content fits (the table still fills the
   pane) and correct when it does not (it grows, and `.rwrap` scrolls it). */
.rtab{width:auto;min-width:100%;border-collapse:collapse;font-size:15px}
.rtab th,.rtab td{padding:9px 14px;text-align:left;vertical-align:middle;white-space:nowrap}
.rtab thead th{font-size:12px;font-weight:500;color:var(--dim);border-bottom:1px solid var(--line);
  padding-top:11px;padding-bottom:8px}
.rtab tbody tr{border-bottom:1px solid var(--line)}
.rtab tbody tr:last-child{border-bottom:0}
.rtab tbody tr:hover{background:var(--soft)}
.rtab th[scope=row]{font-weight:500;color:var(--ink);max-width:220px;overflow:hidden;
  text-overflow:ellipsis}
.rtab .num{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;
  font-size:14px;color:var(--muted)}
.rtab thead .num{font-family:var(--sans)}
.rtab .rval{color:var(--ink);font-weight:600;font-size:14.5px}
.rtab .rnote{font-size:12px;color:var(--dim);font-family:var(--mono)}
/* THE "DOCS" COLUMN IS DELIBERATELY QUIET. Amit's question was "why is disha labels 2 quotes?"
   and the answer is that Offers is the number he wants; Docs is the paper behind it. So Offers
   is an ordinary figure and Docs is grey and a size down - present, checkable, and never
   competing with the count it explains. Hiding it would have been the other mistake: the
   revision is a real thing that happened. */
.rtab .dim{color:var(--dim);font-size:13px}
.rtab thead th.dim{color:var(--dim);opacity:.72}
/* THE NATIVE SUBTOTALS WRAP; THEY DO NOT CLIP AND THEY DO NOT PUSH. Same argument as
   `.rtile .rsub` above, one table over: these are the figures that were never multiplied by
   anything, so they are the evidence for the converted column beside them - and three
   currencies on one line ("€ 388,670 · ₹ 12,85,82,000 · US$ 833,129") is 300px of nowrap that
   shoved Amit's commission column off the right edge. Let the row grow instead. */
/* `min-width` IS LOAD-BEARING HERE, and it is the second half of the phone fix above.
   `overflow-wrap:anywhere` is right for this column - it holds a firm's own untouched
   subtotals, "€ 388,670 · ₹ 11,76,96,500 · US$ 165,349", which must wrap rather than drag the
   table out to its full length. But `anywhere` also tells the browser this column's MINIMUM
   width is one character, so when the table ran short of room it took the offer literally:
   on Amit's phone the header came out 31px wide and 271px tall, one letter per line, and the
   figures beside it 682px tall. A column may wrap; it may not be crushed into a ribbon.
   Below this floor the table grows instead and `.rwrap` scrolls it, which is what the
   comment on `.rwrap` said should happen all along. */
.rtab .rnat{white-space:normal;min-width:168px;max-width:210px;overflow-wrap:anywhere;
  line-height:1.5}
/* The one case where a stage row's note is not a note but a warning: money that is NOT in the
   column beside it. Unreachable by construction (reports_api.py only picks a native currency
   when every valued card is already in it) - and painted in the app's attention colour anyway,
   because "unreachable" is a claim about today's data and a funnel that quietly lost a deal is
   what this page exists to not do. */
.rtab .rnote.warn{color:var(--amber);font-family:var(--sans);font-weight:600}
/* The model rows are the totals and the width rows are their split, so the eye must be able to
   tell them apart without a legend: the model is display-face and heavier, the width is
   indented, small and grey. No chevrons - nothing collapses, so a control that suggests it
   would be a lie. */
.rtab .rmodel th[scope=row]{font-family:var(--display);font-weight:600;font-size:19px;
  letter-spacing:.02em}
.rtab .rmodel td{color:var(--ink)}
.rtab .rmodel{border-bottom:0}
.rtab .rsub-row th[scope=row],.rtab .rsub-row td{padding-top:4px;padding-bottom:4px}
.rtab .rsub-row th[scope=row]{padding-left:26px;font-weight:400}
.rtab .rsub-row .rw{font-size:13px;color:var(--dim)}
.rtab .rsub-row .num{font-size:13px}
.rtab .rsub-row:last-child{border-bottom:0}
.rtab .rfirm{display:block;font-size:12px;color:var(--dim);font-weight:400}
.rbcell{width:180px;min-width:120px}
.rbar{display:block;height:7px;background:var(--soft-2);border-radius:2px;overflow:hidden;
  min-width:80px}
.rbf{display:block;height:100%;background:var(--line-strong);border-radius:2px}
.rbar.strong .rbf{background:var(--ink)}
/* The stage word, in the board's own vocabulary. Won is the app's green and Lost is grey, not
   red: a lost deal is a fact, not an alarm, and Amit's board is a TEMPERATURE - the colours
   run cold to hot and stop. */
.rstage{display:inline-block;font-size:12px;font-weight:600;letter-spacing:.04em;
  text-transform:uppercase;padding:2px 8px;border-radius:3px;background:var(--soft-2);
  color:var(--muted)}
.rstage.s-discussion{background:transparent;color:var(--dim);box-shadow:inset 0 0 0 1px var(--line-strong)}
.rstage.s-quoted{background:var(--soft-2);color:var(--muted)}
.rstage.s-cold{background:var(--blue-wash);color:var(--blue)}
.rstage.s-warm{background:var(--amber-wash);color:var(--amber)}
.rstage.s-hot{background:var(--red-wash);color:var(--red)}
.rstage.s-won{background:var(--green-wash);color:var(--green)}
.rstage.s-lost{background:var(--soft-2);color:var(--dim)}
.rfoot{font-size:12.5px;color:var(--dim);line-height:1.55;margin:0;padding:11px 14px 13px;
  border-top:1px solid var(--line);white-space:normal}
.rfoot b{color:var(--muted);font-weight:600}
/* Commission is Multitec's alone and the page says so rather than relying on the reader to
   notice. A tinted column, not a red one: it is confidential, not wrong. */
.rtab .rcomm{background:var(--soft);color:var(--ink);font-weight:600}
.rcommfoot{background:var(--soft)}
.rempty{font-size:14px;color:var(--dim);padding:14px;margin:0}
/* WHICH CURRENCY THIS PAGE IS IN, AND WHY. Amit: "for people who quote in $, their funnel
   should be in $." Sits directly under the scope note, because they are the same kind of
   sentence - what this page is showing you, before it shows you. It reuses .scopenote whole
   and adds only the gap: a second style here would say the two facts are different weights,
   and they are not. */
.scopenote.curnote{margin-top:5px}
.rnotes .rwrap{overflow-x:visible}
.rnotes ul{margin:0;padding:12px 20px 16px 34px}
.rnotes li{font-size:13.5px;color:var(--muted);line-height:1.6;margin:0 0 7px}
.rnotes li:last-child{margin-bottom:0}
.rnotes b{color:var(--ink);font-weight:600}
.rnotes .em{color:var(--ink);font-style:italic}
/* By month: a column per month, contiguous. The count sits ABOVE its bar rather than in a
   tooltip - a figure you have to hover for is a figure on a phone nobody can read. */
.rmonths{display:flex;align-items:flex-end;gap:5px;padding:16px 14px 12px;min-height:140px}
.rcol{flex:1 1 0;min-width:26px;display:flex;flex-direction:column;align-items:center;gap:4px;
  justify-content:flex-end;height:112px}
.rcv{font-size:11px;color:var(--dim);line-height:1}
.rcb{display:block;width:100%;max-width:44px;background:var(--ink);border-radius:2px 2px 0 0;
  min-height:2px;transition:height .18s ease}
.rcol:hover .rcb{background:var(--red)}
.rcl{font-size:10.5px;color:var(--dim);font-family:var(--mono);line-height:1;white-space:nowrap}
@media(max-width:720px){
  .rtiles{grid-template-columns:repeat(2,minmax(0,1fr))}
  .rtile .rv{font-size:21px}
  .rtab{font-size:14px}
  .rtab th,.rtab td{padding:8px 10px}
  /* The bar is the first thing to go on a phone: it is a comparison, and the figure beside it
     is the fact. Losing the picture costs nothing; losing the number would cost everything. */
  .rbcell{display:none}
  .rtab .rmodel th[scope=row]{font-size:17px}
  .rmonths{min-height:120px}
  .rcol{height:92px;min-width:22px}
}

/* ================= CRM · OPPORTUNITIES =================
   Amit: "within CRM, i need 2 tabs / views / sub menus for now - contacts and opuurtunities.
   we will fold in the kanban board / funnel later on." / "i need to see the chips on the
   oppurtunities, who owns them" / "colourcode the chips so that i can see it from far".

   NO NEW DESIGN LANGUAGE, and almost no new CSS: the list is .rec, the detail is .card/.sec/
   .grid, the chips are .achip, the stages are .pill, the layout is main.md - all of it
   already on this page. What is genuinely new is three things and no more: a heading that is
   also the sub-nav, a full-width chip row inside .rec, and a colour per stage. */

/* The CRM pages' heading. It is `.pagehead h1` and nothing else - the sub-nav that used to
   live in this slot became the app's top nav (index.html), so what is left is a page title
   set in the display face like every other page's. The `.vh` class below is kept: the People
   view still uses it, and a visually-hidden helper earns its place in any stylesheet. */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---------- a card in the list ----------
   .rec's own grid with a THIRD area: the chips get a full-width row of their own rather than
   wrapping under the value, where the eye reads them as part of the money. The stage and the
   value stack right, so a 340px column never has to choose between them. */
.rec.opp{align-items:start}
.rec.opp .rmeta{flex-direction:column;align-items:flex-end;gap:6px}
.ochips{grid-column:1/-1;display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:9px}
.ochips:empty{display:none}
/* nothing issued yet - a fact, said quietly. Not --line-strong (.dash): this is text to read,
   not a rule to look past. */
.rnum.none{color:var(--dim);font-family:var(--sans);font-size:12.5px}

/* ---------- the stage ----------
   THE COLOUR IS A LEGEND FOR THE WORD A HUMAN TYPED. It is not computed, it does not move,
   and nothing on this page ages a card - the decay rule is Amit's and he is still writing it
   (STATUS.md: "the decay logic is NOT built and must not be invented"). If a card is cold it
   is because somebody said so.

   These are .pill (a STATUS: outlined, light, quiet) and never .achip (an IDENTITY: solid,
   shouting). Red for `hot` is not the .achip rule being broken - that rule is about identity
   chips, and status has always been allowed the palette's meanings here (.tag.add is red,
   .tag.pend amber, .tag.ok green). Hot IS attention. */
/* DISCUSSION IS NOT A TEMPERATURE, so it is not given one. Amit: "discussion should be the
   base one and never enters the heat map" - it is the stage a card is born at when it came
   from a phone call, and the palette says so by staying out of the blue-amber-red run
   entirely: dashed, dimmest, the quietest thing on the board. Nothing has been claimed about
   this deal yet, and the chip should not claim one either. */
.pill.st-discussion{color:var(--dim);background:transparent;border-style:dashed;
  border-color:var(--line-strong)}
.pill.st-quoted{color:var(--muted);background:var(--soft);border-color:var(--line-strong)}
.pill.st-cold{color:var(--blue);background:var(--blue-wash);border-color:var(--blue-line)}
.pill.st-warm{color:var(--amber);background:var(--amber-wash);border-color:var(--amber-line)}
.pill.st-hot{color:var(--red);background:var(--red-wash);border-color:var(--red-line)}
.pill.st-won{color:var(--green);background:var(--green-wash);border-color:var(--green-line)}
.pill.st-lost{color:var(--dim);background:var(--soft);border-color:var(--line-strong)}
/* THE STAGE, EDITABLE. A <select> that keeps the .pill/.st-* colours, so the badge does not
   change appearance the moment it becomes clickable - it is the same fact, now movable, and
   a control that redraws itself into a different shape reads as a different thing.

   `appearance:none` because a native select drops a chrome-grey bevel over the stage colour
   on every platform, and the whole point of the pill is that hot is red from across a room.
   The caret is drawn instead, and 18px of right padding leaves it room.

   MIN-HEIGHT 28px, NOT the pill's 2px padding. This is the only stage control in the app and
   on a 390px phone it has to be hittable - a 16px-tall target is a miss, and a miss here
   moves nothing while looking like it should have. Font-size 11px matches the pill; iOS
   zooms a <select> under 16px only on focus of a text field, not on a picker, so the phone
   opens its native wheel rather than scaling the page. Verified at 390x844 in qa/ui.py. */
.pill.stagesel{appearance:none;-webkit-appearance:none;cursor:pointer;
  min-height:28px;padding:2px 18px 2px 7px;max-width:118px;
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
    linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-size:4px 4px,4px 4px;
  background-position:calc(100% - 9px) calc(50% + 1px),calc(100% - 5px) calc(50% + 1px);
  background-repeat:no-repeat}
.pill.stagesel:disabled{opacity:.55;cursor:progress}
.pill.stagesel:focus-visible{outline:2px solid var(--ink);outline-offset:1px}

/* proposed, not confirmed: the agent has said Won/Lost and Amit has not. It is amber because
   it is WAITING - the same voice as every other pending thing in this app. */
.pill.pend-st{color:var(--amber);background:var(--amber-wash);border-color:var(--amber-line);
  text-transform:none;letter-spacing:0}

/* THE ENGINE PUT THIS CARD HERE, and it says so quietly. `--dim` and no fill on purpose: it is
   an ATTRIBUTION, not a status, and it sits beside a pill that already carries the colour. A
   second coloured chip here would read as a second temperature and start an argument with the
   one next to it. Only appears where the engine actually moved something (crm.js:_engineSet) -
   on the fourteen cards that merely sit where they were born, it is absent. */
.autoset{margin-left:5px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--dim);vertical-align:middle;cursor:help}

/* HOLD THE COOLING OFF THIS CARD UNTIL A DATE. Quiet by default because it is the rare act -
   most cards never need one - but a real labelled control rather than a hidden gesture: a
   right-click menu is not an alternative to a button, because nobody finds it. */
.snoozer{display:inline-flex;align-items:center;gap:5px;margin-left:8px;
  font-size:11px;color:var(--dim)}
.snoozer input{font:inherit;color:var(--muted);background:var(--bg);
  border:1px solid var(--line);border-radius:6px;padding:2px 5px}
.snoozer input:hover{border-color:var(--line-strong)}

/* nobody owns it. The one .achip with no hue: an unowned card is not an identity, it is the
   absence of one, and giving it a colour would make it look like a person. */
.achip.none{background:var(--soft-2);color:var(--muted);border:1px dashed var(--line-strong);
  padding:2px 7px}

/* ---------- the quotes and the pursuits on a card: BOTH ARE DOORS, AND BOTH LOOK IT ----------
   There were two rules here - `.qrow{cursor:default}` and `.qrow:hover{background:var(--bg)}` -
   under a comment that read "a quote row is a FACT here, not a door. (The door is the quote
   form, and it is not wired to this list yet.)"

   THEY ARE DELETED, NOT MOVED. That comment stopped being true the day quoteRows() became a
   <button> wired to crmOpenQuote, and these two lines outlived it: the door WORKED and looked
   dead - no pointer, no hover, nothing on screen to suggest it could be pressed. CSS
   contradicting the code is worse than either being wrong alone, because the code is right and
   nobody can tell by looking. Amit: "from the contacts card, i can not go to the quote, the
   opuurtunities - it should be routable from there."

   Nothing replaces them: .rec already carries `cursor:pointer` and the hover tint, and it is
   the shape every other pressable row in this app uses. The fix for a row that is a door is to
   stop suppressing the affordance it already had. */
/* THE TWO AXES, said out loud. Revisions (same offer, rev+1) and cross-quotes (its own
   offer, cast onto another machine) are different things, and the frozen tool's bug was
   conflating them - so they get two marks, not one. Mono, because they are both about
   offer numbers. */
.qaxes{display:flex;gap:6px;flex-wrap:wrap;margin-top:7px}
.qax{font-family:var(--mono);font-size:11px;color:var(--dim);background:var(--soft);
  border:1px solid var(--line);border-radius:3px;padding:1px 6px;white-space:nowrap}
.qax.cross{color:var(--muted);border-color:var(--line-strong)}

/* THE TRACK GOES FULL WIDTH ON A NARROW COLUMN. The tablet gives the list the narrowest
   column it ever gets (270px), and on a phone it is 390px minus padding. An inline-flex track
   sized to its content would sit stranded against the left edge with the three tabs cramped;
   stretched to the column it gets even thirds and the labels have room to breathe. The
   font drops a notch because "Opportunities" is 13 characters and it is the one that decides
   whether all three fit. */
@media(max-width:1040px){
  /* `flex:1 1 auto` and NOT `1 1 0`: basis auto keeps each label's own width in the sum, so
     the three share the leftover space in proportion to their words rather than being forced
     into equal thirds - which is what truncated "Opportunities" to "Opport..." at 270px while
     "People" sat in a pill twice the size it needed. Padding drops to 7px because at the
     tablet's 270px list column the padding, not the type, is what stops all three fitting. */
}

/* ================= THE QUOTES GRID, AND THE PEEK ================= (quotes_tab.js)
   Amit: "maybe the quotes tab should have grid list of all quotes" + "remember to get the
   notion type ui and functionality with our logic and simplicity and ease in this app" +
   "notions manners as well as the grid type, card type look - why not pick from something
   that is proven".

   SO: NOTION'S TABLE, IN MULTITEC'S INK AND TYPE. The layout and the interaction are
   borrowed on purpose - nobody reinvents a table, and inventing one here was the actual
   risk. What is NOT borrowed is anything you can see the colour of: no Notion grey, no
   Notion blue, no Notion face. Every value below is a token from :root.

   What was copied, and it is worth being explicit so nobody "tidies" it back into a card
   list: dense quiet rows on HAIRLINE separators (no boxes, no zebra); column headers small
   and low-contrast with no fill; property chips inline in the cells; hover is a faint tint
   and that is the whole affordance; selected is a quiet persistent tint, NOT a border
   (a border changes the row's size, and this grid must never move).

   THE GRID IS THE PAGE. Full width, no rail, no 340px gutter. It is a FINDER - "where is
   that Etipress quote from March" - and a finder you read across.

   AND IT NEVER MOVES WHEN YOU SELECT. That is why the peek is a FIXED OVERLAY rather than a
   second grid column: a column would resize the table on every click, which is exactly the
   jarring reflow Amit rejected in the CRM ("opening it in the same heightwise column makes
   it look jarring and not like a real app"). quotePick() flips aria-current in place and
   repaints nothing - the same promise .md makes, kept a different way because this list is
   full-bleed and .md's is not. */
main.grid{grid-template-columns:minmax(0,1fr);max-width:none;padding:0}
/* THE PADDING IS ON THE WRAP, NOT ON THE SCROLL CONTAINER, and that is the sticky header
   working rather than a style preference. Padding-top on a scroller is INSIDE the scrollport,
   so `position:sticky; top:0` pins the header below it - and the rows then scroll UP THROUGH
   that strip and appear ABOVE the header. Measured: a 22px band of live rows floating over the
   column titles. Moving the padding inward makes top:0 mean the pane's own edge, so there is
   no strip for anything to show through. */
main.grid > .pane{padding:0}
.gridwrap{max-width:var(--maxw);margin:0 auto;width:100%;padding:22px 28px 60px}

/* ---- the head: title left, the one button right. Nothing else is permanent. ---- */
.gridhead{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;margin:0 0 16px}
.gridhead h1{margin:0;font-family:var(--display);font-weight:600;font-size:25px;
  letter-spacing:-.01em;flex:1 1 auto}
.gridhead .sub{display:block;font-family:var(--sans);font-weight:400;font-size:13px;
  color:var(--dim);letter-spacing:0;margin-top:2px}

/* ---- the toolbar: search + status filter. Quiet, and it is the only chrome above the
        rows. Notion puts these here and so do we. ---- */
.gridbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 6px}
.gridbar input.q{flex:1 1 240px;max-width:340px;min-width:0;padding:7px 10px;
  font:400 14px/1.4 var(--sans);color:var(--ink);background:var(--bg);
  border:1px solid var(--line);border-radius:5px}
.gridbar input.q:focus{outline:none;border-color:var(--line-strong)}
.gridbar input.q::placeholder{color:var(--dim)}
.gridbar .count{margin-left:auto;font-size:12.5px;color:var(--dim);white-space:nowrap}

/* ---- THE FILTERS. Amit: "filters should be able to be put in there" - by agent, by
        salesman, by model, by status.

   NATIVE <select>, and that is the whole design. This app has no bundler and no component
   library, a native select is a real control on a phone (it opens the OS picker), it is
   keyboard-accessible for free, and it cannot be got subtly wrong. A hand-built dropdown
   here would be a gimmick with a focus-trap bug in it.

   EVERY OPTION COMES FROM THE ROWS THE SERVER RETURNED - never a hard-coded list. A filter
   that names a salesman or an agent the caller cannot reach IS a leak, exactly like the chip
   would be: it would tell Bruce that Gaurav exists. And a filter only appears when the
   caller's reach spans more than one value, so a salesman never gets a "Raised by" listing
   himself alone. Both rules live in filterHtml() (quotes_tab.js) and both fall out of the
   data rather than out of a role check. */
.gfilt{display:inline-flex;align-items:center;gap:5px}
.gfilt select{padding:6px 8px;font:400 13px/1.2 var(--sans);color:var(--ink);
  background:var(--bg);border:1px solid var(--line);border-radius:5px;max-width:170px}
.gfilt select:hover{border-color:var(--line-strong)}
.gfilt select:focus{outline:none;border-color:var(--line-strong)}
/* A filter that is DOING something says so - otherwise "why am I seeing 3 of 23" is a
   hunt. Quiet: a tint and the ink, not a badge. */
.gfilt select.on{background:var(--soft-2);color:var(--ink);font-weight:500;
  border-color:var(--line-strong)}
.gclear{border:0;background:none;color:var(--dim);cursor:pointer;font:400 12.5px var(--sans);
  padding:4px 6px;border-radius:4px;text-decoration:underline}
.gclear:hover{color:var(--ink);background:var(--soft)}

/* ---- the date range. The browser's OWN calendar (<input type=date>): no library, no CDN,
        the OS picker on a phone, and it is keyboard-accessible for free. ---- */
.gfilt.dates{gap:4px}
.gfilt .dlbl{font-size:11px;color:var(--dim);letter-spacing:.03em}
.gfilt .dto{font-size:11.5px;color:var(--dim)}
.gfilt input[type=date]{padding:5px 7px;font:400 12.5px var(--sans);color:var(--ink);
  background:var(--bg);border:1px solid var(--line);border-radius:5px;
  font-family:var(--mono);font-size:11.5px}
.gfilt input[type=date].on{background:var(--soft-2);border-color:var(--line-strong)}
.gfilt input[type=date]:focus{outline:none;border-color:var(--line-strong)}

/* ---- the pager. Two arrows and a sentence. NO numbered buttons: at 500 quotes that is 20 of
        them, and nobody has ever wanted page 13 - the filters and the search are how you find
        a quote; this is how you walk what is left. ---- */
.gpager{display:flex;align-items:center;gap:10px;padding:12px 2px 0}
.gpager .pg{font-size:12.5px;color:var(--dim)}
.gpager .qbtn{min-width:34px;font-size:14px;line-height:1;padding:5px 10px}
.gpager .gfilt{margin-left:auto}
.gpager .gfilt select{font-size:12px;padding:4px 7px}

/* The status segmented filter: reuses .seg's mechanics, quieter. */
.segq{display:inline-flex;border:1px solid var(--line);border-radius:5px;overflow:hidden}
.segq button{border:0;background:var(--bg);color:var(--muted);cursor:pointer;
  padding:6px 11px;font:400 13px/1.2 var(--sans);border-right:1px solid var(--line)}
.segq button:last-child{border-right:0}
.segq button[aria-pressed="true"]{background:var(--soft-2);color:var(--ink);font-weight:500}
.segq button:hover{background:var(--soft)}

/* ---- the table itself ---- */
.gtable{width:100%;border-collapse:collapse;table-layout:fixed}
/* THE HEADER: small, low-contrast, NO FILL - Notion's, and the reason it works is that a
   header with a background competes with the data; a header that is merely quieter does not.
   It STICKS: Amit, "the header does not ctick". A finder is scrolled, and a column header
   that scrolls away turns a table into a wall of unlabelled figures.

   border-collapse:collapse DROPS the borders of a sticky cell (the collapsed border belongs
   to the table, which scrolls; the cell is what stays), so the hairline under the header is a
   box-shadow instead. Same 1px, same token, and it survives the stick.
   `background` is not decoration here either: a transparent sticky header lets the rows
   scroll THROUGH it. */
.gtable thead th{position:sticky;top:0;z-index:2;background:var(--bg);
  text-align:left;font:500 11.5px/1.3 var(--sans);color:var(--dim);
  letter-spacing:.03em;padding:7px 10px;box-shadow:inset 0 -1px 0 var(--line);
  white-space:nowrap;-webkit-user-select:none;user-select:none}
.gtable thead th.r{text-align:right}
/* EVERY column sorts. Amit: "should be sortable by any of the headers." */
.gtable thead th.sortable{cursor:pointer}
.gtable thead th.sortable:hover{color:var(--muted);background:var(--soft)}
.gtable thead th .ar{opacity:0;margin-left:3px;font-size:9px}
.gtable thead th[aria-sort] .ar{opacity:1}
.gtable thead th[aria-sort]{color:var(--ink)}

/* THE ROWS. ~36px, hairline between, nothing else. `cursor:pointer` on the whole row: the
   row IS the affordance, exactly as in a Notion table. */
.gtable tbody tr{border-bottom:1px solid var(--line);cursor:pointer}
.gtable tbody tr:hover{background:var(--soft)}            /* the faint tint. that is all. */
/* SELECTED: a persistent tint, never a border or an outline - both change the row's box and
   the grid would jump by a pixel on every click. This is the "it never moves" promise, and
   it is one property. */
.gtable tbody tr[aria-current="true"]{background:var(--soft-2)}
.gtable tbody td{padding:8px 10px;font:400 13.5px/1.35 var(--sans);color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
/* The offer number is the row's identity, so it is ink, mono and the thing your eye lands
   on - the same treatment every other figure in this app gets. */
.gtable td.no{font-family:var(--mono);font-size:12.5px;color:var(--ink);font-weight:500}
.gtable td.cust{color:var(--ink);font-weight:500}
.gtable td.r{text-align:right}
.gtable td.money{font-family:var(--mono);font-size:12.5px;color:var(--ink);text-align:right}
/* THE CURRENCY SYMBOL IS NOT A DECORATION - IT IS THE MOST IMPORTANT CHARACTER IN THE CELL.
   It was `--dim` while the digits beside it were full ink: the one character that prevents the
   single most expensive misread this product can make was deliberately the faintest thing in
   the row. Agents in India, South Africa and Colombia read INR, USD and EUR in one grid.

   THREE CHANGES, NO NEW COLOUR:
   1. full ink, same weight as the digits - it is part of the number, not a label on it;
   2. a FIXED-WIDTH SLOT (3.4ch fits "US$"), so the symbols line up down the column and you
      read the currency vertically, in peripheral vision, without reading a digit;
   3. the digits are untouched - black, mono, tabular.
   Colour was considered and refused: the Indian lakh grouping already gives Rs 3,63,97,500 and
   US$315,877 completely different shapes, so a tint would be a third mechanism for a job two
   already do - and tinting money is how a money tool starts looking like a dashboard toy. */
/* The running total's symbol, same argument: it was --dim beside a 30px figure, so the one
   thing distinguishing a rupee quote from a dollar one was the quietest mark on the panel. */
.total-v .cursym{color:var(--muted);margin-right:6px}
.gtable td.money .cur{color:var(--ink);margin-right:5px;
  display:inline-block;min-width:3.4ch;text-align:right}
.gtable td.mach{font-family:var(--mono);font-size:12px;color:var(--muted)}
.gtable td.when{color:var(--dim);font-size:12.5px}
.gtable .empty{padding:34px 10px;text-align:center;color:var(--dim);font-size:14px}

/* ---- the property chips, in-cell. Notion's "select" property: small, rounded, a SOFT
        TINT with darker text of the same hue. Ours are the same object in our palette -
        and .achip (the agent chip) stays exactly as it is elsewhere, so the CRM and this
        grid agree about whose book a thing is in. ---- */
.pchip{display:inline-block;border-radius:3px;padding:1px 7px;font-size:11.5px;
  font-weight:500;line-height:1.5;white-space:nowrap;max-width:100%;overflow:hidden;
  text-overflow:ellipsis;vertical-align:middle}
/* draft = it is not a document yet; issued = the customer is holding it; superseded = a
   later rev replaced it. Three states, three tints, and only `issued` gets a colour with
   any weight - it is the only one that means something happened. */
.pchip.draft{background:var(--soft-2);color:var(--muted)}
.pchip.issued{background:var(--green-wash);color:var(--green)}
.pchip.superseded{background:var(--soft-2);color:var(--dim);text-decoration:line-through}
/* WHERE IT IS IN THE APPROVAL WORKFLOW - a second axis beside the status above, because a
   submitted draft is still status='draft'. Blue = waiting on a person, green = yes, red =
   handed back; all three from the existing tokens, so dark mode is already answered. Nothing
   is drawn for a plain working draft. */
.pchip.wf-submitted{background:var(--blue-wash);color:var(--blue)}
.pchip.wf-approved{background:var(--green-wash);color:var(--green)}
.pchip.wf-rejected{background:var(--red-wash);color:var(--red)}
.pchip.rev{background:var(--amber-wash);color:var(--amber);font-family:var(--mono);
  font-size:11px}
/* WHO RAISED IT, and WHOSE BOOK. Both tinted by the same agentHue() the CRM's .achip uses -
   DERIVED from an id, never assigned - so one person and one agent keep one colour in every
   view, with nothing to configure and nothing to keep in step.
   `color-mix` gives Notion's soft-fill-with-darker-text in one line and with no second
   palette to maintain: the fill is the hue at 12%, the text is the hue itself.

   WHY NOT .achip HERE, when the CRM's card uses it: .achip is a solid block of colour with
   white uppercase text - right on a card, where it is one chip on a big surface and Amit
   asked to "see it from far". In a 36px row it is a shout, and twenty of them stacked is a
   barcode. The HUE is the identity and it is unchanged; only the fill weight differs, which
   is exactly the difference between a card and a table row. */
.pchip.who,.pchip.firm{background:color-mix(in srgb, var(--h) 12%, transparent);color:var(--h)}
/* The house is not one of the agents, it IS the shop - ink, not a hue. Same reasoning as
   .achip.house and .tag.special, which are already ink for the same reason. */
.pchip.firm.house{background:var(--soft-2);color:var(--muted)}

/* ================= THE PEEK =================
   Notion's side peek: it comes in OVER the page, the page stays put, Escape or a click
   outside sends it away and the row you opened stays marked. Every one of those is a
   promise this app already makes somewhere else (.pop, the phone's .mddetail); this is the
   same idea at the size of a document.

   FIXED, not a grid column - see the note at the top of this block. The scrim is nearly
   invisible on purpose: it is there to catch the click-outside and to push the grid back a
   half-step, not to dim the room. */
.peekscrim{position:fixed;inset:0;z-index:39;background:rgba(var(--shade),.10)}
.peek{position:fixed;top:0;right:0;bottom:0;z-index:40;width:min(780px,64vw);
  background:var(--bg);border-left:1px solid var(--line);
  /* a subtle divider, NOT a heavy shadow: the border is what separates it. The shadow only
     lifts the very edge so the grid does not appear to bleed under it. */
  box-shadow:-18px 0 40px -30px rgba(var(--shade),.5);
  display:flex;flex-direction:column;min-width:0;
  transform:translateX(100%);transition:transform .2s ease;visibility:hidden}
.peek[data-open="1"]{transform:none;visibility:visible}
.peekhead{flex:0 0 auto;padding:16px 20px 12px;border-bottom:1px solid var(--line)}
.peektop{display:flex;align-items:flex-start;gap:12px}
/* The Due drawer's Mine/Everyone switch - the same .seg control as the CRM scope switch,
   sized down to sit in the drawer header between the title and the close button. */
.duescope{margin-left:auto;align-self:center}
.duescope button{font-size:12px;padding:4px 10px}
/* The title is big at the top - Notion's peek, and it is the right instinct: the offer
   number IS the record's name. */
.peekhead h2{margin:0;flex:1 1 auto;min-width:0;font-family:var(--mono);font-weight:600;
  font-size:19px;color:var(--ink);letter-spacing:-.01em}
.peekhead .cust{display:block;font-family:var(--sans);font-weight:400;font-size:13.5px;
  color:var(--muted);margin-top:3px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.peekhead .meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:9px}
.peekx{flex:0 0 auto;border:0;background:none;cursor:pointer;color:var(--dim);
  font-size:20px;line-height:1;padding:2px 6px;border-radius:4px}
.peekx:hover{background:var(--soft);color:var(--ink)}

/* THE ACTION ROW. Quiet, and it is on the OPEN QUOTE - never a toolbar shouting at an empty
   grid (STATUS.md: revisions and cross-quotes "are actions on an open quote; a menu for them
   is a Save As folder"). It does not exist until a quote does. */
.peekacts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:11px}
.peekacts .spacer{flex:1 1 auto}
.qbtn{border:1px solid var(--line-strong);background:var(--bg);color:var(--muted);cursor:pointer;
  padding:5px 10px;border-radius:5px;font:500 12.5px/1.3 var(--sans)}
.qbtn:hover{background:var(--soft-2);color:var(--ink);border-color:var(--ink)}
.qbtn[disabled]{opacity:.45;cursor:default}
.qbtn[disabled]:hover{background:var(--bg);color:var(--muted);border-color:var(--line-strong)}
.qbtn.primary{background:var(--ink);border-color:var(--ink);color:var(--on-solid)}
.qbtn.primary:hover{background:var(--ink);color:var(--on-solid)}
/* Discard: the one button on this row that takes something away. It reads QUIET until you
   reach for it - the same shape as its neighbours, red only on the letterform - because a
   permanently red button in a row of six is noise, and noise is what gets clicked by accident.
   Tokens, never literals (CLAUDE.md): --red-line and --red are the pair that stay legible in
   both themes, and this is a letterform, not a fill, so the brand red is the right one. */
.qbtn.danger:hover{background:var(--bg);border-color:var(--red-line);color:var(--red)}
/* The Issue chooser: the document-style buttons that replace "Issue quote" once pressed. */
.issueas{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
.issueas .issuelbl{color:var(--muted);font:600 12px/1.3 var(--sans);margin-right:1px}
.peekbody{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  background:var(--soft-2)}
.peekbody iframe{display:block;width:100%;height:100%;border:0;background:var(--soft-2)}
.peekmsg{padding:30px 24px;color:var(--muted);font-size:14px;line-height:1.6;
  background:var(--bg)}
.peekmsg b{color:var(--ink)}
.peekmsg.bad{color:var(--red)}
.peekwait{padding:30px 24px;color:var(--dim);font-size:13.5px;background:var(--bg)}
/* iOS stands where the iframe would: WebKit will not scroll a PDF in a subframe, so the
   quote opens in Safari's own viewer instead. See paintPdf() in quotes_tab.js. */
.peekios{padding:30px 24px;background:var(--bg)}
.peekios p{margin:0 0 16px;color:var(--muted);font-size:14px;line-height:1.6;max-width:52ch}
.peekios .qbtn{text-decoration:none}

/* ---- the cross-quote target picker: revealed by the button, never permanent ---- */
.xq{padding:14px 20px;border-bottom:1px solid var(--line);background:var(--soft)}
.xq .row{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap}
.xq label{display:flex;flex-direction:column;gap:3px;font-size:11px;color:var(--dim);
  letter-spacing:.03em}
.xq select,.xq input{padding:5px 7px;font:400 13px var(--sans);color:var(--ink);
  background:var(--bg);border:1px solid var(--line);border-radius:4px}
.xq input{width:62px;font-family:var(--mono)}
.xq .note{margin:8px 0 0;font-size:12px;color:var(--dim);line-height:1.5}

/* ---- the fan-out / revise report. THE HONEST HALF: what dropped loudly, what swapped,
        what the target brought of its own. The agent must see it, not just the new total. ---- */
.rep{padding:16px 20px;background:var(--bg);border-bottom:1px solid var(--line)}
.rep h3{margin:0 0 3px;font-family:var(--display);font-weight:600;font-size:16px;
  color:var(--ink)}
.rep .lead{margin:0 0 12px;font-size:13px;color:var(--muted);line-height:1.55}
.rep .grp{margin:0 0 11px}
.rep .grp:last-child{margin-bottom:0}
.rep .glbl{font-size:10.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--dim);margin-bottom:4px}
.rep .grp.loud .glbl{color:var(--red)}
.rep .grp.gain .glbl{color:var(--green)}
.rep ul{margin:0;padding:0;list-style:none}
.rep li{font-size:12.5px;color:var(--muted);line-height:1.5;padding:2px 0 2px 10px;
  border-left:2px solid var(--line)}
.rep .grp.loud li{border-left-color:var(--red)}
.rep .grp.gain li{border-left-color:var(--green)}
.rep li b{color:var(--ink);font-weight:500}
.rep li .why{color:var(--dim)}
.rep .none{font-size:12.5px;color:var(--dim);font-style:italic}

/* ---- tablet: the peek gives up the fixed width and takes most of the screen ---- */
@media(max-width:1040px){
  main.grid{overflow:visible}
  .gridwrap{padding:18px 20px 60px}
  .peek{width:min(680px,86vw)}
}
/* ---- phone: it comes over the top, full width, with a back arrow. The same overlay, and
        the grid underneath is never unmounted - come back and you are where you were. ---- */
@media(max-width:720px){
  .gridwrap{padding:16px 14px 60px}
  .peek{width:100%;border-left:0}
  .peekscrim{display:none}          /* nothing to click outside of; Back is the way out */
  .peekback{display:inline-flex!important}
  /* ---- THE GRID AT 390: IT STOPS BEING A TABLE. ----
     Five columns earn their place on a phone - the offer number, the customer, the MACHINE,
     the status and the total. The rest is on the quote when you open it.

     THE MACHINE IS NOT OPTIONAL, and the first cut got that wrong. It hid the machine on a
     phone on the theory that the offer number and the customer identify a row. They do not:
     a customer with four quotes is four near-identical lines differing only in a serial
     nobody has memorised, and the one question you ask of this grid - WHICH PRESS IS THIS -
     was the one thing you had to open a quote to answer. Amit: "i only see quote numbers...
     so very difficult to make out what is what". Status comes back for the same reason, in
     the same breath: draft and issued are different KINDS of row, not a detail. */

     AND IT IS NOT A NARROW TABLE, IT IS A STACKED ROW, because a narrow table does not work:
     `display:none` on a <col> is NOT honoured by browsers (only visibility:collapse is, and
     that is buggy), so with table-layout:fixed the hidden columns GO ON RESERVING THEIR
     WIDTHS. Measured: the table computed to 874px inside a 390px viewport and squeezed the
     customer to ZERO WIDTH - the one column a phone most needs. Found by measuring, not by
     looking: it did not overflow the page, it silently ate a column.

     So below 720 the table becomes blocks and the col widths stop applying at all. Same DOM,
     same rows, same handlers - only the CSS changes, which is the rule the .md pattern already
     follows. */
  .gtable,.gtable tbody,.gtable tr,.gtable td{display:block;width:auto}
  .gtable colgroup,.gtable thead{display:none}
  .gtable td.hide-s{display:none}
  /* THREE LINES: offer, customer, machine - identity narrowing as you go down, which is the
     order you actually read them in. The price stays on the right where the eye already
     looks for it, spanning the first two lines; the status chip sits under it on the third,
     so the right-hand column is money-then-state and never a ragged stack.

     `money` named on both of the first two rows is what makes it SPAN them - grid-template-
     areas joins repeats of one name into a single rectangle. That is the whole trick here. */
  .gtable tbody tr{display:grid;grid-template-columns:1fr auto;
    grid-template-areas:"no money" "cust money" "mach stat";column-gap:12px;padding:10px 6px}
  .gtable tbody td{padding:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .gtable td.no{grid-area:no;font-size:13px}
  .gtable td.cust{grid-area:cust;font-size:12.5px;color:var(--muted);font-weight:400;
    margin-top:2px}
  /* MONO AND DIM, like the desktop cell: "S3S · 670mm · 8c" is a specification, and the
     mono makes the three parts line up down the column so the eye can scan widths without
     reading words. Smaller than the customer because it is the third thing you need. */
  .gtable td.mach{grid-area:mach;font-size:11.5px;margin-top:3px}
  .gtable td.stat{grid-area:stat;justify-self:end;align-self:end;margin-top:3px}
  .gtable td.money{grid-area:money;align-self:center;font-size:13px}
  .gtable .empty{display:block;padding:30px 8px}
  .gridbar input.q{flex:1 1 100%;max-width:none}
  .gridbar .count{margin-left:0}
}
.peekback{display:none;border:0;background:none;color:var(--muted);cursor:pointer;
  font:500 13px var(--sans);padding:4px 8px 4px 0;align-items:center;gap:6px}
.peekback:hover{color:var(--ink)}

/* ============ CRM: THE GRID IS THE PAGE — BOTH VIEWS ============ (crm.js)
   Amit: "shouldn't opuurtunities take the large part of the screen rather than the left
   narrow pane (should be like a grid) so that when clicked, it opens the card on the right.
   think notion type design where there is grid view, card view and all" + "notions manners as
   well as the grid type, card type look - why not pick from something that is proven".

   ...and then, of Contacts: "the crm view is not upto the mark. again a list on the left. i am
   talking about the contacts." So this block dresses BOTH views now, and not one line of it
   was forked to do it: Contacts' grid is these very rules, the same .ogrid/.ogrow/.ogtile
   under the same `data-md=wide`, differing only in which columns bookCols() asks for. Two
   answers to "show me a list of things in the CRM" was the complaint; one answer is the fix,
   and a second copy of this CSS under a `cg*` prefix would have re-created the problem in the
   stylesheet a week after it was solved on the screen.

   NOTION'S TABLE, IN MULTITEC'S INK AND TYPE - the same borrowing, and deliberately the same
   NUMBERS, as the Quotes grid above (.gtable / quotes_tab.js). Two grids in one app must look
   like one system, so the header size, the row padding, the hairline, the hover tint and the
   selected tint are all lifted from that block rather than re-decided here. Where they differ
   it is because the DATA differs, never because two people had two opinions.

   ONE DELIBERATE DEPARTURE FROM NOTION, and it is for our own consistency: Notion's real
   table draws hairlines BETWEEN COLUMNS as well as between rows (checked, not remembered -
   their own product screenshot). Ours does not, because the Quotes grid shipped without them
   an hour before this did, and one app that cannot agree with itself about whether a table
   has vertical rules looks worse than one that is a shade less like Notion. The chips give
   these columns all the structure they need.

   CLASS NAMES ARE `og*` ON PURPOSE: .gtable, .gbar, .gallery, .gh and .gc are all
   quotes_tab.js's. Two files, two namespaces, no collision. The `og` was for "opportunities
   grid" and it is now the CRM's grid, both views - the names were left alone deliberately.
   Renaming them to `cg*` would have touched every rule and every call site in two files to
   buy a better initial, and a rename that big is how a class gets missed and a row silently
   loses its hairline. The prefix is a namespace, not a description. */

/* ---- the layout: the list IS the page, the panel is a reserved column ----
   `data-md=wide` is set by renderOpps() AND by renderContacts() - both views are this shape
   now, so nothing clears it any more. The proportions are INVERTED from .md's default (340px
   list + 1fr detail) and that inversion IS the feature - Amit's complaint, twice, was the
   "left narrow pane". The grid takes the 1fr; the panel takes a fixed 320.

   IT IS ALWAYS THERE, and that is what makes the promise keepable: a panel that appeared on
   click would take 340px off the grid at the exact moment the cursor is aiming at a row, and
   every row would re-lay-out underneath it. Reserving the column pays that once, on arrival,
   and then the grid NEVER moves again. See the long note in crm.js:renderOpps. */
main.md[data-md=wide]{grid-template-columns:minmax(0,1fr) 320px}

/* ---- the strip above the rows: the lenses left, the filters right ----
   Notion's own arrangement: what you are looking at sits by the heading, how many rows there
   are sits over the rows. Quiet - no fill, one hairline, and no button that is not a control
   somebody asked for. */
.ogbar{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;
  padding:9px 12px;border-bottom:1px solid var(--line)}
.ogbar:empty{display:none}
/* ＋ New opportunity sits with the view tabs, at the LEFT, and pushes the filters and the
   export icons to the right - `space-between` alone would spread three groups evenly and leave
   the one verb on this page floating in the middle of a row of filters. It is an action, and
   it belongs beside the place you are standing, not among the questions you are asking. */
.ogbar .ognew{margin-right:auto}
/* The view switcher: small, text-y, near the heading - NOT a .seg. A .seg is this app's
   two-states-one-true control (Export/Domestic, Multitec/All agents) and it is a QUESTION.
   Which lens you are reading through is not a question, it is a place you are standing, so it
   gets the shape the shell's nav already uses for that: plain words, the live one marked. */
.ovtabs{display:flex;gap:2px}
.ovtabs button{border:0;background:transparent;cursor:pointer;font-family:var(--sans);
  font-size:13.5px;font-weight:500;color:var(--dim);padding:4px 9px;border-radius:4px;
  border-bottom:2px solid transparent}
.ovtabs button:hover{color:var(--muted);background:var(--soft)}
.ovtabs button[aria-current=true],.ovtabs button[aria-current=true]:hover{color:var(--ink);
  border-bottom-color:var(--ink);background:transparent}
.ogfils{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.ogfil{display:flex;align-items:center;gap:6px;min-width:0}
.ogfil .lbl{white-space:nowrap}
.ogfil select{border:1px solid var(--line-strong);background:var(--bg);color:var(--ink);
  border-radius:4px;font-size:13px;padding:4px 6px;max-width:168px;cursor:pointer}
.ogfil select:hover{border-color:var(--line-strong)}
/* NEEDS ATTENTION - a switch, not a coordinate. Every other control on this bar answers
   "which slice?"; this one answers "show me the neglected ones", so it looks like a button
   that stays pressed rather than another dropdown to read past.

   AMBER WHEN ON, and that is the app's existing vocabulary rather than a new colour: amber
   is "waiting / needs a human" everywhere here (.pill.pend-st, .tag.pend). NOT red - red is
   an error, and a deal nobody has rung is not a fault, it is a job. And never the word
   "dead" or "stale": the vocabulary is cooling (docs/HEAT_AND_DIGEST_PLAN.md). */
.ogattn{border:1px solid var(--line);background:var(--bg);color:var(--muted);
  border-radius:4px;font-size:13px;padding:4px 9px;cursor:pointer;white-space:nowrap;
  min-height:30px;display:inline-flex;align-items:center;gap:6px}
.ogattn:hover{border-color:var(--line-strong);color:var(--ink)}
.ogattn[aria-pressed="true"]{color:var(--amber);background:var(--amber-wash);
  border-color:var(--amber-line);font-weight:600}
.ogattn .qd{font-size:11px;opacity:.75;font-weight:500}

.ogcount{font-size:12.5px;color:var(--dim);white-space:nowrap}

/* ---- the scroller. THE PAGE NEVER SCROLLS SIDEWAYS - the TABLE does, inside its own box.
        That is the rule at 390px and it is why the grid can keep its columns instead of
        squeezing them into unreadability. ---- */
/* THE SIDEWAYS SCROLL HAS TO ANNOUNCE ITSELF. Measured at 390px on Opportunities: 356px
   visible of 896px, so 60% of every row - including the Value column - sat off-screen with no
   scrollbar, no shadow and no clipped-looking edge. It did not read as "there is more this
   way"; it read as "US$406," which is a price with its end cut off, and a half-price is worse
   than no price.
   A fade pinned to the right edge is the hint. It is `background-attachment:local` on the
   scroller itself - the classic Roman Komarov shadow - so it appears only when there IS more
   to the right and disappears as you reach the end, rather than being a decoration that lies
   at both extremes. */
.ogscroll{overflow-x:auto;overscroll-behavior-x:contain;
  background:linear-gradient(to right, var(--bg) 30%, transparent),
             linear-gradient(to left, var(--bg) 30%, transparent) 100% 0,
             linear-gradient(to right, rgba(var(--shade),.10), transparent 12px),
             linear-gradient(to left, rgba(var(--shade),.10), transparent 12px) 100% 0;
  background-repeat:no-repeat;background-size:34px 100%,34px 100%,14px 100%,14px 100%;
  background-attachment:local,local,scroll,scroll}
@media(max-width:720px){
  /* THE NAME STAYS PUT while you scroll to the money. Without this you scroll right, find
     "US$406,376" and have lost which customer it belongs to - so the number is legible and
     meaningless. The first cell is the identifying one in all three grids (Company, Name,
     Customer), which is why this can be a single rule rather than one per view. */
  .oghead > *:first-child,.ogrow > .ogc:first-child{position:sticky;left:0;z-index:2;
    background:var(--bg)}
  .ogrow:hover > .ogc:first-child{background:var(--soft)}
  .ogrow[aria-current=true] > .ogc:first-child{background:var(--soft-2)}
  .oghead > *:first-child{background:var(--soft)}
  /* and a hairline so the frozen column reads as frozen rather than as overlapping text */
  .oghead > *:first-child::after,.ogrow > .ogc:first-child::after{content:"";position:absolute;
    top:0;bottom:0;right:0;width:1px;background:var(--line)}
  .oghead > *:first-child,.ogrow > .ogc:first-child{position:sticky}
}
.ogrid{min-width:100%}
/* stretch, NOT center: the cells are flex boxes that centre their own content, and a cell
   that does not fill the row's height cannot carry a divider or a full-height hover. */
.oghead,.ogrow{display:grid;grid-template-columns:var(--gcols);align-items:stretch}
/* THE HEADER: small, low-contrast, no fill. Same numbers as .gtable thead th. It is sticky so
   that a long board keeps its headings; z-index 1 keeps it over the rows and under the
   app's own header (z-index 30). */
.oghead{border-bottom:1px solid var(--line);background:var(--bg);position:sticky;top:0;z-index:1}
.ogh{display:flex;align-items:center;gap:4px;border:0;background:transparent;cursor:pointer;
  padding:9px 10px;font:500 11.5px/1.3 var(--sans);letter-spacing:.03em;color:var(--dim);
  text-align:left;min-width:0;-webkit-user-select:none;user-select:none}
.ogh .tx{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ogh.r{justify-content:flex-end}
.ogh:hover{color:var(--muted);background:var(--soft)}
.ogh[aria-sort]{color:var(--ink)}
.ogh .car{font-size:9px;flex:0 0 auto}
/* THE ROWS. Hairline between, hover is a faint tint, selected is a persistent tint - never a
   border or an outline, because both change the row's box and the grid must not move by so
   much as a pixel when you click it. */
.ogrow{border:0;border-bottom:1px solid var(--line);width:100%;text-align:left;padding:0;
  background:var(--bg);cursor:pointer;font-family:var(--sans)}
.ogrow:hover{background:var(--soft)}
.ogrow[aria-current=true]{background:var(--soft-2)}
.ogrow[aria-current=true] .ogc.customer .tx{color:var(--red)}

/* ---------- a grid row that is NOT itself one big button ----------
   The People grid (crm.js:pplGridRowHtml). Its row carries THREE things you can press - a
   mailto:, a tel:, and the name that opens the person - so it cannot also be a single
   <button>: an <a> inside a button is invalid HTML, and the click bubbles so tapping the
   number both dials and opens the card. That is the exact bug Amit hit on the company card
   ("it does click on tel: but also opens the edit pane"), and this is the shape that cannot
   have it. The row keeps the hairline, the hover tint and the selected tint - it still reads
   as a row - it just stops claiming to be a control.

   Contacts and Opportunities keep their button rows: their cells are text, so one big target
   is right there and there is nothing inside to fight it. */
.ogrow.static{cursor:default;display:grid}
.ogrow.static:hover{background:var(--soft)}
.ogtile.static{cursor:default}
/* THE NAME IS THE DOOR. A button that has to look like the text beside it - same face, same
   size - because in a grid a row of blue links is noise; what marks it is that it goes red
   and underlines when you point at it, exactly as the customer cell in the other two grids
   goes red when its row is open. */
.oglink{border:0;background:transparent;padding:0;margin:0;cursor:pointer;
  font-family:inherit;font-size:inherit;font-weight:500;color:var(--ink);text-align:left;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
.oglink:hover{color:var(--red);text-decoration:underline}
.oglink:focus-visible{outline:2px solid var(--red-bright);outline-offset:2px;border-radius:2px}
.ogrow.static[aria-current=true] .oglink{color:var(--red)}
/* The links inside a cell must not inherit .ogc's nowrap-ellipsis fight - they do their own. */
.ogc .tel,.ogc .mail{font-size:12.5px;min-height:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;max-width:100%}
/* the cards lens */
/* SCOPED TO `.ogtile.static` - the People cards - and the scoping is a bug fix, not tidiness.
   These were written as bare `.ogtile .tprops`, which is the CONTACTS tile's selector too. The
   Contacts rule further down sets display/flex-wrap/gap/align-items but NOT flex-direction, so
   `column` leaked through and stacked a card footer that is laid out as a row: the money and
   the "1 person · 1 offer · yesterday" line printed on top of each other. Amit caught it on
   the Cards lens.
   A People tile stacks its email and phone deliberately - each is a tap target and wants its
   own line - so the rule is right, it just had to say which tile it meant. */
.ogtile.static .tname{font-size:14.5px;font-weight:600;line-height:1.35}
.ogtile.static .tsub{font-size:12.5px;color:var(--muted);line-height:1.45}
.ogtile.static .tprops{display:flex;flex-direction:column;align-items:flex-start;gap:3px;
  font-size:12.5px}
/* Amit: "whenever there are labels like this grow, the row." A .pill is nowrap with no
   max-width, so two of them cannot fit a 74px cell - and without flex-wrap the overflow was
   not even clipped, it painted over the next column. min-height + align-items:stretch means
   wrapping grows the row by itself. Shared fix: .pres, .ochips, .tprops and .ctl each carry
   their own flex-wrap already - every chip container wrapped except the generic grid cell. */
.ogc{display:flex;align-items:center;flex-wrap:wrap;gap:5px;min-width:0;min-height:37px;
  padding:6px 10px}
.ogc.r{justify-content:flex-end}
.ogc .tx{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font:400 13.5px/1.35 var(--sans);color:var(--muted)}
.ogc.customer .tx{color:var(--ink);font-weight:500}
.ogc .tx.num{font-family:var(--mono);font-size:12.5px;color:var(--ink)}
.ogc .tx.when{color:var(--dim);font-size:12.5px}
.ogc .tx.none{color:var(--dim)}
/* the chips have to give way before the text does - a name clipped to "Mart…" still names a
   man. 37px is the row's FLOOR, not its height: a cell carrying two pills wraps and the row
   grows with it (see .ogc above). */
.ogc .achip{max-width:100%;font-size:10px;padding:2px 6px}
.ogc .pill{font-size:10px;padding:1px 5px}
/* ---- TWO CURRENCIES, TWO LINES, NEVER ONE SUM ----
   The one new mark the Contacts grid needed, and it exists because of a rule rather than a
   look: a firm can hold US$, EUR and INR offers at once, each pinned to its own fx, and there
   is no true total across them (crm.js:lifetimeMoney, and CLAUDE.md). The server sends
   by_currency; the cell prints one line per currency. A row that held them side by side would
   read as an expression - "$344,654 ₹2,65,13,500" is two facts that look like arithmetic - and
   a row that added them would be a lie the eye cannot catch.

   The row grows for it (min-height is a MINIMUM), which is the honest cost: a two-currency
   customer is a taller row, and there are very few of them. */
.ogc .vstack{display:flex;flex-direction:column;align-items:flex-end;gap:1px;min-width:0}
.ogc .vstack .tx{line-height:1.3}
.ogtile .vstack{display:flex;flex-direction:column;align-items:flex-start;gap:1px;min-width:0}

/* ---- the other lens: Notion's gallery. Tiles in a responsive grid, hairline border, no
        shadow, generous padding. The SAME rows and the SAME selection - only the shape
        changes, which is why nothing here is fetched and nothing here is a second list. ---- */
/* 260px, up from 216. THE TILE HAS A REAL MINIMUM and 216 was below it: the footer is a
   money figure beside "2 people · 1 offer · yesterday", both `nowrap`, which need ~233px
   before the gap - so at 216 they printed on top of each other. It never showed while the
   list shared the screen with a reserved detail column, because the narrower container fitted
   fewer, wider tiles; giving the grid the full width fitted MORE tiles and squeezed each one
   under its own minimum. Amit caught it on the Cards lens.
   Sized from what the footer actually needs rather than nudged until it looked right. */
.ogallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;
  padding:14px}
.ogtile{display:flex;flex-direction:column;align-items:stretch;gap:9px;text-align:left;
  background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:13px 14px;
  cursor:pointer;font-family:var(--sans);min-width:0}
.ogtile:hover{background:var(--soft)}
.ogtile[aria-current=true]{background:var(--soft-2);border-color:var(--line-strong)}
.ogtile .tn{font-size:14.5px;font-weight:500;color:var(--ink);line-height:1.35;
  overflow-wrap:anywhere}
.ogtile[aria-current=true] .tn{color:var(--red)}
.ogtile .ts{font-size:12.5px;color:var(--dim);line-height:1.45;overflow-wrap:anywhere;
  margin-top:-4px}
.ogtile .tprops{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
/* A tile whose properties are all absent must not keep their gap. Contacts' tiles carry only
   an agent chip and a retired pill, and both legitimately vanish (one agent on screen says
   nothing; most firms are live) - leaving .ogtile's own 9px gap around an empty box. The same
   rule .ochips:empty already keeps, one list along. */
.ogtile .tprops:empty{display:none}
/* `flex-wrap` is the guard, not the layout: the two halves sit on one line at any sane width,
   and if something ever squeezes the tile below what they need they stack rather than
   overlap. An overlap is unreadable; a wrap is merely tighter. */
.ogtile .tfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;
  gap:4px 8px;border-top:1px solid var(--line);padding-top:8px;margin-top:auto}
.ogtile .tx.num{font-family:var(--mono);font-size:12.5px;color:var(--ink)}
.ogtile .tx.none{font-size:12px;color:var(--dim)}
.ogtile .tw{font-size:11.5px;color:var(--dim);white-space:nowrap}
.ogtile .achip{max-width:100%;font-size:10px;padding:2px 6px}
.ogtile .pill{font-size:10px;padding:1px 5px}

/* ---- below 1040: there is not room for a grid AND a 340px column, so the panel becomes the
        overlay it already is on a phone.
        THIS IS THE SAME RULE AT A DIFFERENT WIDTH, NOT A SECOND PATTERN: the panel is a
        column when there is room for one and an overlay when there is not. The threshold
        moved from 720 to 1040 for this view alone because a GRID needs more room than the
        list Contacts shows - at 834 the two columns would leave the table 486px and it would
        scroll horizontally for the rest of its life. Same markup, same mdOpen/mdClose, same
        back arrow: only the number changed. ---- */
@media(max-width:1040px){
  main.md[data-md=wide]{grid-template-columns:minmax(0,1fr)}
  main.md[data-md=wide] > .mddetail{position:fixed;inset:0;z-index:40;background:var(--soft);
    padding:0 16px 40px;overflow-y:auto;overscroll-behavior:contain;
    transform:translateX(100%);transition:transform .22s ease;visibility:hidden}
  main.md[data-md=wide][data-open="1"] > .mddetail{transform:none;visibility:visible}
  main.md[data-md=wide] .mdback{display:flex;align-items:center;gap:8px;position:sticky;top:0;
    z-index:1;width:calc(100% + 32px);margin:0 -16px;padding:13px 16px;
    border:0;border-bottom:1px solid var(--line);background:var(--bg);cursor:pointer;
    font-family:var(--sans);font-size:15px;font-weight:500;color:var(--muted);text-align:left}
  main.md[data-md=wide] .mdback::before{content:"←";font-size:17px;line-height:1;
    color:var(--red);font-weight:700}
  main.md[data-md=wide] .mdback:hover{color:var(--ink)}
  main.md[data-md=wide] > .mddetail .pagehead:empty{display:none}
  main.md[data-md=wide] > .mddetail > #crmDetailBody{padding-top:16px}
  body.mdopen{overflow:hidden}
}
/* ---- phone: the bar stacks. The grid itself is unchanged and scrolls inside .ogscroll -
        the columns keep their widths rather than being squeezed to nothing, and the PAGE
        still does not move sideways. Cards are one tap away and are the better read at
        390px, which is what the switcher is for. ---- */
@media(max-width:720px){
  .ogbar{gap:10px;padding:9px 10px}
  .ogfils{width:100%;gap:8px}
  .ogfil{flex:1 1 auto}
  .ogfil select{flex:1 1 auto;max-width:none}
  .ogcount{width:100%}
  .ogallery{grid-template-columns:1fr;padding:12px;gap:10px}
}

/* ---- the panel's head STACKS in the reserved column ----
   `.sec` is a flex row (title left, controls right) and that is right everywhere it is wide.
   In a 320px panel it is not: the chips take the right-hand half and the title gets what is
   left, which turned "Clomark South Africa (Pty) Ltd" into one word per line. Stacking it
   also happens to be Notion's peek exactly - the title big at the top, the properties
   beneath it - so this is the borrowed layout, not a patch on top of one. Scoped to this
   panel: every other .sec in the app has the width it was designed for. */
main.md[data-md=wide] > .mddetail .sec{flex-direction:column;align-items:flex-start;gap:9px}
main.md[data-md=wide] > .mddetail .sec .ctl{justify-content:flex-start}

/* ============================================================ THE OPPORTUNITY WORKSPACE
   "Open wide" — Amit's chosen answer to "the side interaction panel will grow and a long list
   which needs to be scrolled is not the right design." When a card is open the grid recedes and
   the record fills the width as two columns: a summary RAIL (properties) and a roomy ACTIVITY.
   "The record becomes the screen."

   SCOPED TO OPPORTUNITIES (data-view=opps). Contacts shares this #crmMain and its data-md=wide,
   so keying on data-md alone would swallow it too - and Contacts must stay the reserved-column
   master-detail it is. data-open (mdOpen/mdClose, already tracking OPP.open here) is the CLOSED
   vs OPEN switch.

   DESKTOP ONLY, on purpose. At <=1040 the detail is the existing fixed overlay - the record is
   already the whole screen there - so the block below lives inside min-width:1041 and leaves
   that tested overlay exactly as it was. */
@media(min-width:1041px){
  main.md[data-md=wide]{grid-template-columns:minmax(0,1fr)}
  /* CLOSED: THE GRID IS THE PAGE, FULL WIDTH. No reserved column standing empty beside it.
     Opportunities has worked this way since Amit asked for it; Contacts and People did not,
     and he came back for them: "the two levels and only 2 card on contacts and pick a person
     to see thier card on people is wasting screen real estate. not needed. get rid of it and
     extend the useful data as much as you can reasonably and safely".

     The empty pane was the whole cost - a third of a 1400px screen reserved to print "Pick a
     person to see their card", while the grid it was explaining got squeezed into the rest
     and dropped its Email and Phone columns for want of room. Now every column the data
     earns is on screen, and the pane appears only when there is a record to put in it. */
  main.md[data-md=wide]:not([data-open="1"]) > .mddetail{display:none}
  /* OPEN: the grid recedes; the workspace is the single column. The grid stays MOUNTED
     (display:none, never removed), so returning keeps its scroll position and its selection -
     nothing re-fetches, nothing re-lays-out. */
  main.md[data-md=wide][data-open="1"] > .mdlist{display:none}
  /* THE WAY OUT. On desktop the back button is display:none for every master-detail because you
     just click another row - but here the grid is hidden, so it appears as the record's own
     toolbar. Sticky because with the list gone this and Escape are the only routes back, and
     neither should be somewhere you have to scroll to find. Its word follows the view
     (setBackLabel), so it reads "← Contacts", "← People" or "← Opportunities". */
  main.md[data-md=wide][data-open="1"] .mdback{
    display:flex;align-items:center;gap:7px;position:sticky;top:0;z-index:2;width:100%;
    margin:0 0 18px;padding:10px 0 11px;border:0;border-bottom:1px solid var(--line);
    background:var(--soft);cursor:pointer;text-align:left;
    font:500 13.5px/1.4 var(--sans);color:var(--muted)}
  main.md[data-md=wide][data-open="1"] .mdback:hover{color:var(--ink)}
  main.md[data-md=wide][data-open="1"] .mdback::before{content:"←";
    color:var(--red);font-weight:700;font-size:15px;line-height:1}
}
/* The two columns of the record. minmax(0,760) CAPS the activity so lines stay a comfortable
   length and the pair sits left with room around it - the "wide, roomy" of the mockup is
   whitespace, not 1,000px-long sentences. align-items:start so the rail does not stretch to the
   activity's height. */
.oppws{display:grid;grid-template-columns:300px minmax(0,760px);gap:30px;align-items:start}
.wsrail{display:flex;flex-direction:column;gap:16px;min-width:0}
.wsmain{min-width:0}
.wshead{font:600 12px/1 var(--display);letter-spacing:.1em;text-transform:uppercase;
  color:var(--dim);margin:4px 0 15px}
.wsnext .tlnext{margin:0}          /* the rail's own gap owns the spacing */
/* PHONE / TABLET: inside the full-screen overlay, the two columns stack - summary on top,
   activity below, full width. */
@media(max-width:1040px){
  .oppws{grid-template-columns:1fr;gap:18px}
}

/* The "view as" warning: you are looking at an issued quote in a mode it was NOT issued in.
   Red, because --red means "attention" everywhere in this app and this is the one thing in
   the viewer that can mislead a person about what a customer actually holds. */
.viewas{padding:12px 20px;background:var(--red-wash);border-bottom:1px solid var(--line);
  font-size:12.5px;line-height:1.55;color:var(--muted)}
.viewas b{color:var(--red)}
.viewas .qbtn{margin-left:6px;padding:3px 8px;font-size:12px}

/* ---- the date range, the pager: the two controls paging brought with it ----
   Amit: "the quotes list should be paginated, should be able to filtered in a date range
   (date pickers) and so on". Both are quiet by construction - a pager you notice is a pager
   in the way. */
.ogdates{gap:5px}
.ogdates input[type=date]{border:1px solid var(--line-strong);background:var(--bg);color:var(--ink);
  border-radius:4px;font-family:var(--sans);font-size:12.5px;padding:3px 5px;cursor:pointer;
  min-width:0;width:126px}
.ogdates input[type=date]:hover{border-color:var(--line-strong)}
.ogdates .to{font-size:12px;color:var(--dim)}
/* the × only exists while a range is set - it is an undo, not furniture */
.ogclear{border:0;background:transparent;color:var(--dim);cursor:pointer;font-size:15px;
  line-height:1;padding:2px 5px;border-radius:4px;font-family:var(--sans)}
.ogclear:hover{background:var(--soft);color:var(--red)}
/* THE COUNT IS THE POINT OF THE PAGER, not the arrows: "1-25 of 312" is the sentence that
   stops this grid ever quietly showing 0.1% of the data and calling it the data. */
.ogpager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  padding:9px 12px;border-top:1px solid var(--line)}
.ogpager:empty{display:none}
.ognav{display:flex;align-items:center;gap:4px}
.ognav button{border:1px solid var(--line);background:var(--bg);color:var(--muted);
  cursor:pointer;border-radius:4px;font-family:var(--sans);font-size:14px;line-height:1;
  padding:3px 9px}
.ognav button:hover:not([disabled]){border-color:var(--line-strong);color:var(--ink)}
.ognav button[disabled]{opacity:.35;cursor:default}
.ogp{font-size:12.5px;color:var(--dim);padding:0 6px;white-space:nowrap}
/* ROWS PER PAGE - the count hugs the left, the nav and this selector pack to the right, so
   three children read the same as the two did (auto margin beats space-between). Matches the
   Quotes grid's own per-page control. */
.ogpager .ogcount{margin-right:auto}
.ogper select{border:1px solid var(--line-strong);background:var(--bg);color:var(--muted);
  border-radius:4px;font-family:var(--sans);font-size:12px;padding:4px 7px;cursor:pointer}
.ogper select:hover{border-color:var(--line-strong);color:var(--ink)}
.ogper select:focus{outline:none;border-color:var(--line-strong)}
/* a header that cannot honestly be sorted is TEXT, not a dead button - see oppGridHtml */
.ogh.plain{cursor:default}
.ogh.plain:hover{background:transparent;color:var(--dim)}
@media(max-width:720px){
  .ogdates{flex:1 1 100%}
  .ogdates input[type=date]{flex:1 1 auto;width:auto}
}

/* ---- a quote on a card is a DOOR now, not a fact ----
   It was `.qrow{cursor:default}` with hover suppressed, because "a quote row is a FACT here,
   not a door - the door is the quote form, and it is not wired to this list yet". It is wired
   now (Amit: "i would like to hyperlink the quote... navigation around the app should be
   natural"), so the row gets the affordance every other .rec in this app has: a pointer, a
   faint hover, and the offer number in red on hover because the offer number IS the link.
   Overrides the two rules above rather than editing them - they are still right for any
   .qrow that is not a button. */
button.qrow{cursor:pointer;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;
  align-items:start;width:100%;text-align:left;border:0;border-bottom:1px solid var(--line);
  background:var(--bg);font-family:var(--sans)}
button.qrow:hover{background:var(--soft)}
button.qrow:hover .rn{color:var(--red)}
/* THE META STACKS, AND THAT IS WHAT KEEPS THE OFFER NUMBER READABLE. This is .rec.opp's rule
   (line ~1257) arrived at the hard way a second time, so it is written down here too: the row
   is `minmax(0,1fr) auto`, and an `auto` track claims its content laid out on ONE line. In a
   side panel 298px wide, a pill + "₹4,06,57,500" + "▼ ₹31,00,000" side by side is 244px of
   it - so the 1fr track, which may shrink to nothing, shrank to nothing, and the offer number
   wrapped ONE CHARACTER PER LINE. Nothing was wrong with the data or the markup; the discount
   span (the last thing added to .rmeta) was simply the straw.
   Stacking makes the auto track as wide as the WIDEST ONE ITEM instead of the sum, which is
   the property that matters: the next fact anyone adds to a quote row costs a line of height,
   never the whole left column. Do not "restore" the horizontal meta to save vertical space. */
button.qrow .rmeta{flex-direction:column;align-items:flex-end;gap:5px}

/* ================ THE COMPANY'S LIFETIME INTERACTION (crm.js: lifetimeHtml) ================
   Amit: "why do contacts not show the quotes issued to it? it can be a nice way to see the
   lifetime interaction with that company."

   NO NEW DESIGN LANGUAGE, and on purpose: the four cards are .card/.sec/.grid, the stats are
   .fld/.lbl/.fact (the same object the address above them is drawn with), the quote rows are
   the button.qrow the opportunity panel already uses, the chips are .achip and the stages are
   .pill. The whole feature adds exactly TWO rules, below. A history that arrived with its own
   look would read as a different app bolted onto the card - which is the "shabby ai work"
   Amit has thrown out before, and it is avoided by reaching for what is here rather than by
   restraint applied afterwards. */

/* A third line under a .fact: what the number MEANS. "4 offers" is the fact; "7 quote
   documents · one revised" is the thing that stops the fact being misread as seven quotes -
   which is the exact misreading Amit caught on the opportunities grid. .fld is already a flex
   column with a gap, so this needs no layout of its own; it needs to be quieter than the
   number it explains and louder than nothing. */
.statsub{font-size:11.5px;color:var(--dim);line-height:1.45;margin-top:-1px}

/* WHO RAISED IT, on the company card's quote rows only. It sits in .qaxes beside the Rev and
   Cross-quote marks, but it is NOT of the same kind: those two are about offer NUMBERS, which
   is why .qax is mono. A person's name in JetBrains Mono reads as a serial - so it keeps the
   chip's shape and takes the page's own sans back, with no border, because it is an
   attribution rather than a mark on the offer. */
.qax.who{font-family:var(--sans);font-size:11.5px;background:transparent;border-color:transparent;
  color:var(--dim);padding-left:0}

/* FOUR STATS, FOUR COLUMNS. The shared .grid reserves a 196px minimum per field, which is
   right for an address (a street can be long) and wrong for these: "4", "US$751,030" and
   "17 July 2026" are short, so 196px bought three columns and left Last contact orphaned on
   a row of its own - a ragged shape that reads as a layout bug rather than as a summary.
   150px fits all four across the detail pane at every width above the phone.

   SCOPED TO min-width:721px ON PURPOSE. app.css's own `@media(max-width:720px){.grid{
   grid-template-columns:1fr}}` stacks every field on a phone, and `.grid.lifegrid` would
   out-specify it and quietly un-stack this one card at 390. Overriding only above the
   breakpoint leaves that rule the last word where it matters. */
@media(min-width:721px){
  .grid.lifegrid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
}

/* ============================================================ THE TIMELINE and WHAT I OWE
   timeline.js. What happened on a card, and what is next.
   Amit: "one timeline but the quotes and oppurtunities hyperlinked inline (or be able to
   pick) for that interaction".

   THE DESIGN PROBLEM HERE IS THE OPPOSITE OF THE USUAL ONE. A timeline invites decoration -
   avatars, coloured cards, an icon per kind, a big vertical rail - and every one of those
   makes it slower to READ, which is the only thing it is for. So: one hairline rail, one
   4px dot, and the typed entries in the page's ink while the app's own are in --dim. The
   difference between "Martin rang them" and "the app issued a quote" is a WEIGHT, not a
   colour and not a box.

   FOUR FAMILIES, NOT TWELVE COLOURS: what was said, what was sent, what it cost, and the
   app's own. Twelve hues would be a legend to memorise; four are a glance. No red anywhere -
   red means "attention" everywhere else in this app (.achip), and a logged call is not an
   alarm. */
.tlpanel{padding:2px 0 8px}
.tlempty{margin:10px 2px;font-size:13.5px;line-height:1.6;color:var(--dim);max-width:56ch}
.tlempty.bad{color:var(--amber)}
.tlmini{border:0;background:none;padding:0;margin:0;cursor:pointer;
  font:500 12px/1.4 var(--sans);color:var(--muted);border-bottom:1px solid var(--line-strong)}
.tlmini:hover{color:var(--ink);border-bottom-color:var(--ink)}
.tlmore{margin:12px 2px 2px;font-size:12.5px;color:var(--dim)}

/* ---- the next step. THE ONE THING ON THE PANEL THAT IS ABOUT THE FUTURE, so it sits on
   top: everything below it already happened and is not going to change. */
.tlnext{border:1px solid var(--line);border-radius:8px;padding:11px 13px 12px;
  background:var(--soft);margin:2px 0 14px}
.tlnext.over{background:var(--red-wash);border-color:var(--red-line)}
.tlnext.now{background:var(--amber-wash);border-color:var(--amber-line)}
/* NOT `.q` and `.w`. app.css already has a bare, global `.q` - the quantity box on the
   pricing sheet - which is mono, centred and full-width, and it bled straight through into
   the next step's sentence: "Call Pieter back" rendered as a centred monospace slab. A
   two-letter class name in a 1,700-line stylesheet with no build step is a collision waiting
   to be found by eye, which is exactly how this one was found. */
.tlnext .txt{margin:5px 0 0;font-size:14px;line-height:1.5;color:var(--ink)}
.tlnext.none .txt{color:var(--dim);font-size:13.5px}
.tlnext .row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin:8px 0 0}
.tlnext .on{font-family:var(--mono);font-size:11.5px;color:var(--dim)}
/* The due word is the fact; the date beside it is the receipt. "3 days overdue" is what a
   person acts on - a bare 2026-07-14 makes them do the subtraction themselves, every time. */
.duechip{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.02em;
  padding:2px 7px;border-radius:999px;border:1px solid var(--line-strong);
  color:var(--muted);background:var(--bg)}
.duechip.over{color:var(--red);background:var(--bg);border-color:var(--red-line)}
.duechip.now{color:var(--amber);background:var(--bg);border-color:var(--amber-line)}
.duechip.later{color:var(--dim)}
/* A decision coming back to the submitter: approved (green) or returned (red). */
.duechip.good{color:var(--green);background:var(--bg);border-color:var(--green-line)}
.duechip.bad{color:var(--red);background:var(--bg);border-color:var(--red-line)}

/* ---- the composer. COLLAPSED BY DEFAULT NOW. Amit looked at the panel and said it "will
   grow and a long list which needs to be scrolled is not the right design" — and the composer,
   always-open with its chips, offer picker, when and next-step rider, was eating the top of
   every card before you had read a line of the story. You READ this far more than you WRITE, so
   it is one quiet "＋ Log an interaction" button; the form appears on click and puts itself away
   again after a successful log (the post-save repaint restores the collapsed default — see
   timeline.js:tlComposerHtml). No field and no behaviour was removed; only the default
   visibility moved. (This reverses the earlier "the whole point is that it is already open" —
   one click to start writing is the right cost once reading is the common case.) */
.tlcompose{margin:0 0 16px}
.tllog{width:100%;display:flex;align-items:center;gap:8px;border:1px dashed var(--line-strong);
  border-radius:8px;background:var(--bg);color:var(--muted);cursor:pointer;padding:11px 13px;
  font:500 13.5px/1.4 var(--sans);text-align:left}
.tllog:hover{border-color:var(--ink);color:var(--ink);background:var(--soft)}
.tllog .plus{font-size:15px;line-height:1;color:var(--red)}
.tlnew{border:1px solid var(--line);border-radius:8px;padding:10px 11px 11px;
  background:var(--bg)}
.tlkinds{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:9px}
.tlkind{border:1px solid var(--line-strong);background:var(--bg);color:var(--muted);cursor:pointer;
  padding:3px 9px;border-radius:999px;font:500 12px/1.45 var(--sans);white-space:nowrap}
.tlkind:hover{border-color:var(--line-strong);color:var(--ink)}
.tlkind[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--on-solid)}
.tlline{width:100%;border:1px solid var(--line-strong);border-radius:6px;padding:8px 10px;
  font:400 14px/1.5 var(--sans);color:var(--ink);background:var(--bg);resize:none;
  overflow:hidden;min-height:38px}
.tlline:focus{outline:none;border-color:var(--line-strong);
  box-shadow:0 0 0 3px rgba(var(--shade),.05)}
.tlmeta{display:flex;gap:10px;flex-wrap:wrap;margin-top:9px}
.tlf{display:flex;flex-direction:column;gap:3px;min-width:0}
.tlf select,.tlf input{border:1px solid var(--line-strong);border-radius:5px;padding:4px 7px;
  font:400 12.5px/1.4 var(--sans);color:var(--ink);background:var(--bg);max-width:100%}
.tlnextbox{display:flex;align-items:center;gap:7px;margin-top:10px;
  font-size:12.5px;color:var(--muted);cursor:pointer}
.tlnextfields{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
.tlnextfields input{border:1px solid var(--line-strong);border-radius:5px;padding:5px 8px;
  font:400 13px/1.4 var(--sans);color:var(--ink);background:var(--bg)}
.tlnextfields input[type="text"]{flex:1 1 220px;min-width:0}
/* THE MESSAGE GIVES WAY, NOT THE BUTTON. Flex items shrink by default, so a long enough
   sentence beside Save squeezed it until "Log it" broke over two lines - visible the moment
   tlBad started naming BOTH missing fields in one sentence on a 390px screen. The two controls
   are fixed; .tlmsg takes what is left and wraps, which is what a sentence is for. */
.tlgo{display:flex;align-items:center;gap:11px;margin-top:11px}
.tlgo .tlsave,.tlgo .tlmini{flex:0 0 auto}
.tlgo .tlmsg{min-width:0}
.tlsave{border:0;background:var(--ink);color:var(--on-solid);cursor:pointer;padding:6px 14px;
  border-radius:6px;font:600 13px/1.4 var(--sans)}
.tlsave:hover{background:var(--ink-bg)}
/* SAVING, AND IT LOOKS IT. The request and the repaint after it take about a second on a live
   link, and for that whole second the form is still on screen with the sentence still in it -
   which is how one note reached the timeline twice on 2026-08-25. timeline.js:tlSaving disables
   the button for the whole of it; this is what disabled has to LOOK like, or a button that
   cannot be pressed is indistinguishable from one that was and did nothing. */
.tlsave:disabled{opacity:.55;cursor:progress}
.tlsave:disabled:hover{background:var(--ink)}
.tlmsg{font-size:12.5px;color:var(--dim)}
.tlmsg.bad{color:var(--red)}
/* WHAT IS STILL MISSING, ON THE BOX ITSELF. Amit: "i was trying to put the next due without a
   comment, but it would not let me save. then i realized the comment needs to be in there. so
   highlight all such fields when i press save so that i am not guessing what needs to be
   filled." The sentence beside Save named the rule and never the box; this names the box.

   RED IS RIGHT HERE and nowhere else on this rail - the timeline's own dots are deliberately
   never red (see TL_FAMILY: "a logged call is not an alarm"), which is exactly what leaves red
   free to mean "attention" on this form, as it does everywhere else in the app. The wash as
   well as the border, because a 1px outline on a date input is nearly invisible against the
   native control's own chrome. */
.tlbad,.tlnextfields input.tlbad{border-color:var(--red);background:var(--red-wash)}
.tlbad:focus{outline:none;border-color:var(--red);box-shadow:0 0 0 3px rgba(var(--red-rgb),.12)}
/* CORRECTING AN ENTRY IS THE SAME FORM, IN PLACE ON THE RAIL. Amit: "when i click edit in log,
   open the full thing ... everything that looks like i am doing it for the first time but with
   the previous details filled in." So there is no second stylesheet for it either - .tlnew is
   the form, and these three rules only seat it in the story where the entry was. */
.tlnew.editing{margin-top:7px}
.tle.editing{padding-bottom:18px}
.tle.editing .kind{color:var(--red)}

/* ---- the story. ONE HAIRLINE RAIL, and the dots sit ON it. */
.tlist{list-style:none;margin:0;padding:0;position:relative}
.tlist::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;width:1px;
  background:var(--line)}
.tle{position:relative;display:flex;gap:13px;padding:0 0 15px}
.tle .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;margin-top:5px;
  background:var(--ink);box-shadow:0 0 0 3px var(--bg);position:relative;z-index:1}
.tle[data-fam="send"] .dot{background:var(--blue)}
.tle[data-fam="cost"] .dot{background:var(--amber)}
/* THE APP'S OWN ENTRIES ARE QUIETER, and this is Amit's brief in one rule: "auto entries are
   visually quieter than typed ones, but live in the same column". Hollow, dimmer, smaller -
   present when you scan for them, silent when you are reading what a person did. */
.tle.auto .dot{background:var(--bg);border:1.5px solid var(--line-strong);
  width:8px;height:8px;margin-top:6px}
.tle .body{flex:1 1 auto;min-width:0}
.tle .top{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;line-height:1.4}
.tle .kind{font-size:12px;font-weight:600;color:var(--ink);letter-spacing:.01em}
.tle.auto .kind{font-weight:500;color:var(--dim)}
.tle .by,.tle .when{font-size:11.5px;color:var(--dim)}
/* "(by Amit)" after the acted-as name. Dimmer than the name it follows, because the question
   the eye is asking is WHOSE deal this is - who pressed the key is the footnote, and it is
   only ever present when the two people differ. A token, never a literal (CLAUDE.md). */
.tle .actedby{color:var(--dim);opacity:.75}
.tle .dot-sep{font-size:11px;color:var(--line-strong)}
.tle .edited{font-size:10.5px;color:var(--dim);border:1px solid var(--line);
  border-radius:3px;padding:0 4px}
/* THEY CAME TO US. Shown only when initiated_by = 'them', because agreement is silent: "we
   contacted them" is the ordinary case and badging all hundred rows teaches the eye to skip
   the line. Blue, not red - an inbound approach is good news, and red means "attention"
   everywhere else in this app. Same reasoning as .tle .dot's four families. */
.tle .inbound{font-size:10.5px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;
  color:var(--blue);background:var(--blue-wash);border:1px solid var(--blue-line);border-radius:3px;padding:0 4px}

/* WHO MADE THE MOVE, in the composer. Two radios on one line; they wrap on a phone rather
   than squeezing, because the labels are sentences and a squeezed sentence is the .rnat bug. */
.tlwho{display:flex;flex-wrap:wrap;gap:6px 14px;margin:6px 0 2px}
.tlwhoo{display:inline-flex;align-items:center;gap:5px;font-size:12.5px;color:var(--muted);
  cursor:pointer;min-height:28px}
.tlwhoo input{margin:0;cursor:pointer}
.tlwhoo:has(input:checked){color:var(--ink);font-weight:500}

.tle .what{margin:3px 0 0;font-size:14px;line-height:1.55;color:var(--ink);
  overflow-wrap:anywhere}
.tle.auto .what{font-size:13px;color:var(--muted)}
.tle .links{margin-top:6px}
/* NOTHING IS EVER DELETED (CLAUDE.md), so a struck entry is struck - not gone. It stays
   legible: a record you cannot read is a record you have deleted with extra steps. */
.tle.void .what{text-decoration:line-through;color:var(--dim)}
.tle.void .dot{background:var(--bg);border:1.5px solid var(--line-strong)}
.tle .voidnote{margin:4px 0 0;font-size:11.5px;color:var(--dim);line-height:1.5}
.tle .acts{display:flex;gap:12px;margin-top:6px;opacity:0;transition:opacity .12s}
.tle:hover .acts,.tle:focus-within .acts{opacity:1}
@media(hover:none){.tle .acts{opacity:1}}

/* ---- the quote link. Amit: "hyperlinked inline". Mono, because it is an offer number and
   every offer number in this app is mono - the chip in the grid, the tag on the cover, this.
   It calls quotes_tab.js's openQuote(): THE ONE VIEWER, never a second. */
.tlq{display:inline-flex;align-items:baseline;gap:7px;border:1px solid var(--line);
  background:var(--soft);cursor:pointer;padding:3px 9px;border-radius:5px;max-width:100%}
.tlq:hover{border-color:var(--ink);background:var(--bg)}
.tlq .n{font-family:var(--mono);font-size:12px;font-weight:600;color:var(--ink)}
.tlq .m{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}

/* ============================================================ the Due button and drawer */
/* A BUTTON IN THE HEADER, not a nav item - it is a question you ask on your way past. It
   right-aligns against the session block rather than joining the nav, which is sections. */
.duebtn{flex:0 0 auto;display:inline-flex;align-items:center;gap:7px;cursor:pointer;
  border:1px solid var(--line);background:var(--bg);color:var(--muted);
  padding:5px 11px;border-radius:999px;font:500 13px/1.4 var(--sans)}
.duebtn:hover{border-color:var(--line-strong);color:var(--ink)}
.duen{min-width:18px;height:18px;padding:0 5px;border-radius:999px;
  background:var(--soft-2);color:var(--muted);
  font-size:11px;font-weight:600;display:inline-flex;align-items:center;
  justify-content:center}
/* RED ONLY WHEN SOMETHING IS ACTUALLY LATE. Due-today is not an emergency, and a control
   that is red every day is a control nobody sees. */
/* THE COUNT ON THE SIGNED-IN IDENTITY. timeline.js:tlPaintWhoBadge.
   It sits half off the button's top-right corner - the universal place for this - so it reads
   as attached to the person rather than as another control in the row. `position:relative` on
   the host button is what anchors it; the #role buttons had none, and a badge on a static
   parent would have flown to the header's corner.
   RED ONLY WHEN SOMETHING IS ACTUALLY LATE: --amber for "due", --red for "overdue". Painting
   everything red is how a badge stops meaning anything, which is the same argument as never
   showing a zero. */
/* ---------- the identity: a trigger and a menu ----------
   A dropdown, not a strip of four. The badge below anchors to the TRIGGER, which is why it
   carries position:relative and the menu items no longer need it. */
.whowrap{position:relative}
.whobtn{position:relative;display:inline-flex;align-items:center;gap:9px;cursor:pointer;
  border:1px solid var(--line-strong);background:var(--bg);border-radius:8px;
  padding:7px 11px;font:600 13.5px var(--sans);color:var(--ink);white-space:nowrap}
.whobtn:hover{border-color:var(--ink)}
.whobtn[aria-expanded=true]{border-color:var(--ink);background:var(--soft-2)}
/* A caret drawn rather than typed: "▾" renders at a different size in every font on every
   platform, and a header is the one place that shows. */
.whocaret{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid var(--dim);flex:0 0 auto}
.whobtn[aria-expanded=true] .whocaret{border-top-color:var(--ink)}
/* THE MENU DROPS DOWN, which is the whole of Amit's ask ("open others when clicked vertically
   down instead of horizontal"). Right-aligned to the trigger because the trigger is the last
   thing in the header - a left-aligned menu would hang off the page edge. */
.whomenu{position:absolute;top:calc(100% + 6px);right:0;z-index:70;display:flex;
  flex-direction:column;align-items:stretch;gap:2px;min-width:200px;padding:5px;
  background:var(--bg);border:1px solid var(--line-strong);border-radius:10px;
  box-shadow:0 10px 30px rgba(var(--shade),.13)}
.whomenu[hidden]{display:none}
.whomenu button{display:flex;align-items:center;justify-content:space-between;gap:14px;
  width:100%;border:0;background:transparent;cursor:pointer;text-align:left;
  padding:9px 11px;border-radius:6px;font:500 13.5px var(--sans);color:var(--muted);
  white-space:nowrap}
.whomenu button:hover{background:var(--soft);color:var(--ink)}
.whomenu button:focus-visible{outline:2px solid var(--red-bright);outline-offset:-2px}
/* THE LIVE ONE IS MARKED WITH A TICK, NOT A FILL. A filled row inside an open menu reads as
   the row your cursor is on, so the fill and the hover fight for the same meaning - and the
   first version of this inherited `.seg`'s black fill and painted the name out entirely. */
.whomenu button[aria-pressed=true]{color:var(--ink);font-weight:600}
.whomenu button[aria-pressed=true]::after{content:"✓";color:var(--red);font-weight:700}
/* "What's due" - a destination, not an identity, so it is above the rule and carries the
   count it used to carry in the header. */
.whoitem .duen{position:static;min-width:20px;height:18px;padding:0 6px;
  background:var(--amber);color:var(--on-solid)}
.whoitem .duen.over{background:var(--red)}
/* ---------- the split pill: mine | team ----------
   Amit: "break the pill into two halves - left half for me and right half for the team - only
   for the admins of the agents and superadmins (by reach). for general sales people - they
   only see thier own and a full pill."
   A SEAM, not two controls: one row, one border, a hairline down the middle. Two separate
   buttons would read as two features; a divided pill reads as one question with two answers,
   which is what it is. Each half is its own target and opens the drawer already showing that
   list. The salesman's row keeps `.whoitem` alone and never grows a seam. */
.whoitem.split{padding:0;gap:0;overflow:hidden;border:1px solid var(--line-strong);
  flex-wrap:wrap}
.whoitem.split .duehalf{display:flex;align-items:center;justify-content:space-between;gap:8px;
  flex:1 1 0;min-width:0;padding:8px 11px}
.whoitem.split .duehalf + .duehalf{border-left:1px solid var(--line-strong)}
/* APPROVALS TAKES A ROW OF ITS OWN, and it is not a preference - it is what stops the numbers
   sitting on top of each other. Amit, 2026-08-04: "when i get the approvals notification, it
   is sticking to the other ones. there is an overlap there." Three segments were sharing the
   width two were drawn for (the menu is a fixed 276px), so each got 92px while "Approvals" +
   its count needs ~112 - the red circle spilled across the seam onto "Mine" and the last one
   was clipped by this box's own overflow:hidden.
   A FULL-WIDTH ROW, not a narrower label: approvals is a DIFFERENT OBLIGATION from a follow-up
   falling due (timeline.js says so where it counts them apart), and stacking says that where
   squeezing it into a third of a pill said the opposite. The seam below it is the same
   hairline the halves use, so it is still one control, not two. */
.whoitem.split .duehalf.appr{flex:1 0 100%;border-bottom:1px solid var(--line-strong)}
.whoitem.split .duehalf.appr + .duehalf{border-left:0}
.whoitem.split .duehalf:hover{background:var(--soft)}
.whoitem.split .duek{font-size:12.5px;font-weight:600;color:var(--muted)}
.whoitem.split:hover{background:transparent}   /* the HALVES highlight, not the whole row */
/* The Approvals segment, counted apart from due. Its number is always red (a block is urgent
   already), and the label names it so a glance says which kind is waiting. */
.whoitem.split .duehalf.appr .duek{color:var(--red)}
.whoitem .duen.appr,.whoitem.split .duehalf.appr .duen{background:var(--red-solid);
  color:var(--on-red-solid)}
.duen[hidden]{display:none}
.whosep{height:1px;background:var(--line);margin:5px 4px}
#role button{position:relative}
.whobtn .whodue{position:absolute;top:-7px;right:-7px;min-width:18px;height:18px;padding:0 5px;
  border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:10.5px;font-weight:700;line-height:1;
  background:var(--amber);color:var(--on-solid);border:2px solid var(--bg);pointer-events:none}
.whodue.over{background:var(--red-solid);color:var(--on-red-solid)}
.duebtn.over{border-color:var(--red-line);color:var(--red)}
.duebtn.over .duen{background:var(--red-solid);color:var(--on-red-solid)}

/* ---------- APPEARANCE, in the identity menu ----------
   A LABEL AND THREE WORDS, on one row, because it is the only control in this menu that is a
   CHOICE rather than a destination - everything else here goes somewhere or does something.
   Giving it the same full-width row treatment would have read as "pressing this signs me into
   something", which is the mistake the menu's own note is already about.

   IT DOES NOT USE `.whomenu button`'s ✓ AFFORDANCE. That rule appends a red tick to any
   `[aria-pressed=true]` in this menu and is how the identity list shows who you are; three
   segments with a tick on one of them, inside a bordered group, is two ways of saying the same
   thing in eleven pixels. `::after{content:none}` turns it off for this control only. */
.whotheme{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:7px 11px}
.whotheme .wholbl{font-size:12.5px;color:var(--dim);white-space:nowrap}
.themeseg{display:flex;border:1px solid var(--line-strong);border-radius:5px;overflow:hidden;
  flex:0 0 auto}
.whomenu .themeseg button{display:block;border:0;background:var(--bg);color:var(--muted);
  font:500 12px/1 var(--sans);padding:6px 9px;cursor:pointer;min-height:0;gap:0}
.whomenu .themeseg button + button{border-left:1px solid var(--line-strong)}
.whomenu .themeseg button:hover{background:var(--soft);color:var(--ink)}
.whomenu .themeseg button[aria-pressed=true]{background:var(--ink);color:var(--on-solid);
  font-weight:600}
.whomenu .themeseg button[aria-pressed=true]::after{content:none}

/* The drawer IS the quote peek - same class, same transition, same width rule. One way of
   putting a panel over a page; a second would be a second thing to learn. */
.duedrawer{width:min(560px,52vw)}
.duesub{margin:7px 0 0;font-size:12px;color:var(--dim)}
/* .peekhead h2 IS MONO, and rightly so THERE: the peek's title is an offer number, and every
   offer number in this app is mono. Nothing in this drawer is one - "What I owe" and a
   customer's name are words. Inheriting the peek's shell without inheriting its title face
   is the whole reason this override exists rather than a second header component. */
.duedrawer .peekhead h2{font-family:var(--sans);font-size:18px;letter-spacing:0}
.duedrawer .cardname{font-size:17px;font-weight:600}
/* THE SECTION HEADING IN THE DRAWER - "Waiting for you to approve" / "Due". It appears ONLY
   when both sections are on screen at once (tlApprHtml returns "" when nothing is waiting),
   because a lone heading over the only list is a label for something that needs no label.
   Quiet by design: this is a divider, not a second title competing with the drawer's own. */
.duesec{margin:16px 2px 6px;font-size:11.5px;font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;color:var(--dim)}
.duesec:first-child{margin-top:2px}
.duelist{list-style:none;margin:0;padding:0}
.duerow{border-bottom:1px solid var(--line)}
.duerow:last-child{border-bottom:0}
.duehit{display:flex;gap:14px;width:100%;text-align:left;border:0;background:none;
  cursor:pointer;padding:12px 4px;font:inherit;color:inherit}
.duehit:hover{background:var(--soft)}
/* DECIDE IN THE DRAWER (send for approval). The Approve/Reject controls and the reason box
   sit under the row body. .tlmini.ok / .danger colour the two verbs; the reason box is a
   plain inline row that appears only when Reject is pressed. */
.apprdecide{display:flex;align-items:center;gap:16px;padding:0 4px 12px 118px}
.apprdecide .tlmini.ok{color:var(--green);border-bottom-color:transparent}
.apprdecide .tlmini.ok:hover{border-bottom-color:var(--green)}
.apprdecide .tlmini.danger{color:var(--red);border-bottom-color:transparent}
.apprdecide .tlmini.danger:hover{border-bottom-color:var(--red)}
.apprmsg{font-size:12px;color:var(--muted)}
.apprreason{display:flex;flex-direction:column;gap:8px;padding:0 4px 12px 118px}
.apprreason[hidden]{display:none}
.apprreason .tlline{width:100%;font:inherit;font-size:13px;padding:7px 9px;
  border:1px solid var(--line);border-radius:7px;background:var(--bg);color:var(--ink)}
.apprreasonbtns{display:flex;gap:16px}
/* A decided-quote reminder the agent can dismiss (Case B): the open-button grows, the ✕ sits
   at the end. Hiding the nudge here does not touch the quote. */
.duerow.apprdec{display:flex;align-items:stretch}
.duerow.apprdec .duehit{flex:1 1 auto;min-width:0}
.apprdismiss{flex:0 0 auto;border:0;background:none;cursor:pointer;color:var(--dim);
  font-size:15px;line-height:1;padding:0 12px}
.apprdismiss:hover{color:var(--ink)}
/* THE DATE COLUMN IS FIXED-WIDTH so the customers line up. A list you scan for "who" must
   not have "who" starting at a different x on every row. */
.dueleft{flex:0 0 104px;display:flex;flex-direction:column;gap:4px;align-items:flex-start}
.dueon{font-size:11px;color:var(--dim)}
.duemain{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px}
.duemain .cust{font-size:14px;font-weight:600;color:var(--ink);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.duemain .act{font-size:13px;color:var(--muted);line-height:1.5;overflow-wrap:anywhere}
.duemain .chips{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:3px}
/* The owner chip, coloured by agentHue() - the same hash and the same colours the board and
   the quotes grid use, so a person is the same colour wherever you meet them. Amit:
   "colourcode the chips so that i can see it from far". */
.tlowner{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;
  border:1px solid var(--line);color:var(--dim);background:var(--soft)}
.tlowner.firm{font-weight:500}

@media(max-width:1040px){
  .duedrawer{width:min(560px,72vw)}
}
@media(max-width:720px){
  /* On a phone the drawer is the page - a 52vw panel over a 390px screen is a sliver. The
     peek does exactly this; same pattern, same breakpoint. */
  .duedrawer{width:100vw}
  /* THE WORD STAYS. The first cut dropped it at 390 to save room, and the result was a bare
     "1" in a pill floating beside the wordmark - a badge with no noun, which reads as a
     notification you cannot identify rather than as a control you can press. The header
     already wraps at this width, so the room was never the constraint. Found by looking at
     the screenshot, not by measuring. */
  .duebtn{padding:4px 9px;font-size:12.5px}
  .dueleft{flex-basis:88px}
  .tlmeta{flex-direction:column;gap:8px}
  .tlf select,.tlf input{width:100%}
}

/* A RETYPED PRICE, admin only. It must look like the figure it replaces - not like a
   form field shouting for attention - so it is the same type at the same weight, with
   only a dotted underline to say "you may change this". `.was` beside it is the
   catalog price, struck through, which is the whole point: the original is never lost. */
input.ovr{font-family:var(--mono);font-size:inherit;color:inherit;text-align:right;
  background:transparent;border:0;border-bottom:1px dotted var(--line-strong);
  border-radius:0;padding:0 0 1px;width:100%;max-width:11ch}
input.ovr:hover{border-bottom-color:var(--ink)}
input.ovr:focus{outline:0;border-bottom:2px solid var(--red)}

/* ---------- THE PER-DEAL UPLIFT, on the rail (index.html #upliftRow) ----------
   Amit: "give the tick option and the increase % field on the right side were the floating
   total price comes." It is a `.mini` row like Machine and Accessories, so it inherits their
   type, colour and rhythm and cannot drift from them - the only additions are the tick, the
   narrow field, and the amount printed in the same amber the Discount row uses.

   AMBER, AND THAT IS DELIBERATE. `--amber` already means "money moved on this quote by a
   human" on the row directly below (Discount/Markup). An uplift is the same kind of fact
   pointing the other way, so it reads as one family rather than a new colour with a new
   meaning. A literal here would be a colour that cannot have a second opinion. */
.mini.upliftrow .k{gap:4px}
.mini.upliftrow label{display:flex;align-items:center;gap:5px;cursor:pointer;
  white-space:nowrap}
input.upinput{font-family:var(--mono);font-size:inherit;color:inherit;text-align:right;
  background:transparent;border:0;border-bottom:1px dotted var(--line-strong);
  border-radius:0;padding:0 0 1px;width:3.5ch;margin-left:2px}
input.upinput:hover{border-bottom-color:var(--ink)}
input.upinput:focus{outline:0;border-bottom:2px solid var(--red)}
.mini.upliftrow #upAmt{color:var(--amber)}
/* On a phone the rail is a sticky bottom bar and the thumb is the pointer, so the tick and
   the field both need a real target rather than a 14px one. */
@media (max-width:700px){
  .mini.upliftrow label input[type=checkbox]{width:18px;height:18px}
  input.upinput{width:4.5ch;font-size:15px}
}

/* ---------- ONE FIRM, ONE STACK (crm.js:oppStacksHtml) ----------
   Two pursuits with the same customer used to sit side by side in the gallery, identical at a
   glance. Stacked inside one bordered group with a count on top, the duplication is the first
   thing you see. Amit: "when i look at a company, i should know how many ops i have with them
   and how much time they are wasting of mine or not".

   IT IS A GROUP, NOT A PILE. A literal shuffled-cards effect (offset, rotated, peeking edges)
   would hide the cards behind the front one, and the whole point is that you can see and open
   each of them - "when clicked, then open the two (or 3 or 4 cards seperately)". So they are
   listed, full size, inside a frame that says how many there are. Nothing is hidden and
   nothing needs a second click to reveal. */
.oppstack{grid-column:span 1;display:flex;flex-direction:column;gap:8px;
  border:1px solid var(--line-strong);border-radius:8px;padding:8px;background:var(--soft-2)}
.oppstack .ogtile{background:var(--bg)}
.stackhat{display:flex;align-items:baseline;justify-content:space-between;gap:8px;
  padding:1px 4px 0}
/* The count is the point of the frame, so it is the one thing in it set in ink. */
.stackn{font:600 11.5px var(--sans);color:var(--ink);letter-spacing:.01em}
/* "1 still open" only appears when some of them are NOT - three pursuits of which two are won
   is a good customer; three of which none has moved is the one wasting your time. One number
   cannot say both, so the second only shows when it differs. */
.stacko{font:500 11px var(--sans);color:var(--muted)}

/* ---------- a group in the GRID (crm.js:oppGridRowsHtml) ----------
   Amit: "why dont i see grouping in the grid. thats an easy chevron which expands" - and,
   on the cards: "what if there are 7-8 open persuits? it would meake the card so long."
   Both lenses now fold: the count is the headline, the rows are the detail. */
.gchev{flex:0 0 auto;width:14px;margin-right:2px;color:var(--dim);font-size:10px;
  line-height:1;cursor:pointer;user-select:none}
.ogrow:hover .gchev{color:var(--ink)}
/* The indent that says "this belongs to the row above". Same width as the chevron it sits
   under, so the customer names line up in a column rather than stepping. */
.gchild{flex:0 0 auto;width:14px;margin-right:2px}
.ogrow.gkid{background:var(--soft-2)}
.ogrow.gkid .ogc.customer .tx{color:var(--muted)}
.ogrow.glead .ogc.customer .tx{font-weight:600}
/* The count rides in the customer cell, after the name - it is a fact ABOUT this customer,
   so it belongs beside them rather than in a column of its own that would be empty on every
   ungrouped row. */
.gcount{flex:0 0 auto;margin-left:8px;font:500 10.5px var(--sans);color:var(--dim);
  white-space:nowrap}
/* "+6 more" / "Show fewer" in the cards lens. A real button here - it sits between tiles,
   not inside one, so there is nothing to nest illegally. */
.stackmore{border:0;background:transparent;cursor:pointer;padding:6px 4px 2px;
  font:600 11.5px var(--sans);color:var(--muted);text-align:left}
.stackmore:hover{color:var(--ink);text-decoration:underline}

/* ---------- SIGN IN ---------- */
.loginwrap{position:fixed;inset:0;z-index:60;display:flex;align-items:center;
  justify-content:center;background:rgba(var(--shade),.42);padding:20px}
.loginwrap[hidden]{display:none}
.logincard{background:var(--bg);border:1px solid var(--line-strong);border-radius:6px;
  padding:22px 24px;width:100%;max-width:360px;box-shadow:0 12px 40px rgba(var(--shade),.18)}
.logincard h2{margin:0;font-size:19px;font-weight:600;color:var(--ink)}
.loginsub{margin:2px 0 18px;font-size:12.5px;color:var(--dim)}
.loginlab{display:block;font-size:12px;font-weight:600;letter-spacing:.03em;
  text-transform:uppercase;color:var(--muted);margin:12px 0 5px}
.logininput{width:100%;padding:9px 11px;font:400 14px/1.4 var(--sans);color:var(--ink);
  background:var(--bg);border:1px solid var(--line-strong);border-radius:4px}
.logininput:focus{outline:none;border-color:var(--ink)}
/* The server's own words, never a guess - a wrong password and a disabled account are
   different sentences and the user is owed the right one. */
.loginerr{margin:12px 0 0;font-size:13px;color:var(--red);line-height:1.45}
.loginerr[hidden]{display:none}
.loginrow{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}

/* ---------- the login controls on a Team row ----------
   A row of its own, spanning .prow's grid, because these five buttons beside Edit/Remove
   expanded the third column until the name column collapsed to one letter per line. */
.prow .acct{grid-column:1/-1;display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  padding:8px 0 2px;border-top:1px dashed var(--line)}
.prow .acct:empty{display:none}
.prow .acct .btn{padding:3px 9px;font-size:12.5px}

/* ---------- HIS DEVICES, the Gmail question ----------
   Amit: "active as in when we see gmail ... which session is active from which ip", and
   "he could be logged in from multiple devices". Collapsed by default: the Team screen is a
   roster, and this is a security question asked occasionally rather than a column.
   Every colour here is a token - a literal would be a colour with no second opinion. */
.dtog{display:inline-flex;align-items:center;gap:6px}
.dtog.open{background:var(--soft-2)}
.dsum.none{font:400 12px var(--sans);color:var(--dim)}
/* THE DOT IS THE WHOLE POINT OF THE COLLAPSED STATE - it is what he reads without asking. */
.ddot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:var(--line-strong)}
.dtog.live .ddot,.dev.live .ddot{background:var(--green);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 22%,transparent)}
.devs{grid-column:1/-1;margin:2px 0 4px;padding:8px 10px;background:var(--soft);
  border:1px solid var(--line);border-radius:5px}
.dev{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto auto;
  align-items:center;gap:10px;padding:5px 0;font:400 12.5px var(--sans);color:var(--muted)}
.dev + .dev{border-top:1px solid var(--line)}
.dname{color:var(--ink);font-weight:600;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
/* The address is monospaced so two of them can be compared down the column by eye, which is
   the only reason anybody reads an IP off a screen. */
.dip{font:400 12px var(--mono);color:var(--dim);white-space:nowrap}
.dwhen{white-space:nowrap;color:var(--muted)}
.dev.live .dwhen{color:var(--green);font-weight:600}
.devs .btn{padding:2px 8px;font-size:12px}
.dfoot{margin-top:8px;padding-top:8px;border-top:1px dashed var(--line);
  font:400 11.5px/1.5 var(--sans);color:var(--dim)}
@media (max-width:640px){
  /* THE ADDRESS AND THE COUNT GO FIRST, not the name or the button. A phone still has to
     answer "who is on right now, and can I stop them" - the rest is detail. */
  .dev{grid-template-columns:auto minmax(0,1fr) auto auto}
  .dip{display:none}
}

/* ---------- the signed-out gate ----------
   Signing out used to leave you INSIDE the app with every pane trying to load without an
   identity, so the screen filled with the server's own developer text ("Add a Supabase
   service key to ~/.multitec-cpq/config.json"). That message was written for whoever is
   building this, and a salesman should never meet it. The gate covers the app entirely:
   there is nothing behind it to fail. */
.loginwrap.gate{background:var(--soft);backdrop-filter:none}
.loginwrap.gate .logincard{max-width:380px;text-align:center}
.loginmark{display:block;height:34px;margin:0 auto 18px;width:auto}
.loginwrap.gate .loginlab{text-align:left}
.loginwrap.gate .loginrow{justify-content:stretch}
.loginwrap.gate .loginrow .btn.primary{flex:1 1 auto}
.gatehint{margin:16px 0 0;font-size:12.5px;color:var(--dim);line-height:1.5}

/* ---------- NOTHING IS SHOWN BEFORE WE KNOW WHO YOU ARE ----------
   Amit: "at login it first shows martin for 1-2 seconds and then switches to my
   amit@multitec.in ... this is not professional." He was right twice over: the chip named a
   stranger, and on sign-out the whole app flashed before the sign-in card replaced it.
   The shell is hidden until boot has decided - signed in, or gated - and shell.js sets
   data-ready in a finally, plus a timeout, so a failure can never leave a blank page. */
body:not([data-ready]) header,
body:not([data-ready]) main{visibility:hidden}

/* ---------- show/hide a password ----------
   Amit: "add an eye on the password field so that people can check what they are typing is
   ok or not." A word, not a pictogram: "Show"/"Hide" says which way it will go, where an eye
   glyph leaves you guessing whether it means the current state or the action. */
.pwwrap{position:relative;display:block}
.pwwrap .logininput{padding-right:64px;width:100%}
.pweye{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;background:none;
  cursor:pointer;font:600 11.5px/1 var(--sans);letter-spacing:.04em;text-transform:uppercase;
  color:var(--muted);padding:6px 8px;border-radius:3px}
.pweye:hover{color:var(--ink);background:var(--soft)}

/* ============================================================ PRINT AND DOWNLOAD ICONS
   Amit: "have a download icon and a printer icon on the top right (or appropriate place) of
   all contacts, ops, quotes, reports".

   TOP RIGHT OF THE VIEW'S OWN BAR, on all four, and the same markup everywhere
   (shell.js:Exports.barHtml) so they cannot drift into four slightly different controls.

   ICONS AND NOT WORDS, but never icons ALONE: each carries a `title` and an `aria-label`
   spelling out that it exports exactly what is filtered and sorted on this screen. That
   sentence is the feature - a printout of the wrong slice is worse than no printout - and it
   must be readable by somebody who has never seen the button before. */
.ogexp{display:flex;align-items:center;gap:2px;margin-left:auto}
.ogicon{display:inline-flex;align-items:center;justify-content:center;
  /* 32px, and it is a floor rather than a look: a 15px glyph is a 15px tap target on a
     phone, and this bar sits beside a date picker and three dropdowns. The same reasoning
     the stage control's tap target got. */
  width:32px;height:32px;padding:0;border:1px solid transparent;border-radius:5px;
  background:transparent;color:var(--dim);cursor:pointer;flex:0 0 auto}
.ogicon:hover{color:var(--ink);border-color:var(--line);background:var(--soft)}
.ogicon:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
.ogicon[disabled]{opacity:.45;cursor:default}
/* WORKING, not spinning. An export is one fetch and it either lands or it says why; a
   spinner would imply progress this has no way to measure. */
.ogicon.working{color:var(--ink);border-color:var(--line);background:var(--soft)}

/* A TRANSIENT MESSAGE - see shell.js:toast for why an export needs one at all. Fixed to the
   bottom so it never moves the grid it is reporting about; above .card.big's mobile total
   bar (z-index 50) so it is not hidden behind it on a phone. */
.toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,14px);z-index:80;
  max-width:min(520px,92vw);padding:11px 15px;border-radius:7px;
  background:var(--ink-bg);color:#fff;font-size:13.5px;line-height:1.45;
  box-shadow:0 6px 24px rgba(var(--shade),.24);
  opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}
.toast[data-on="1"]{opacity:1;transform:translate(-50%,0)}
.toast.bad{background:var(--red)}
@media (max-width:720px){
  /* Clear of the sticky running-total bar, which is the one thing on a phone that must never
     be covered - it is the price. */
  .toast{bottom:88px}
}

/* ---------- the salesman's LOGIN address, on the Team roster ----------
   Amit: "why dont you show the email of the salesmen, why cant we edit them?"

   It sits on the same subtitle line as the phone, after a separator, because it is the same
   KIND of fact - how you reach this person - and giving it its own row would push the five
   account buttons down again (the collapse that once printed "Bruce Ferreira" one letter per
   line). It WRAPS rather than truncating: half an email address is not an email address, and
   this one is the login, so a reader has to be able to check it character by character. */
.rs .rsep{margin:0 6px;color:var(--line-strong)}
.rmail{color:var(--dim);text-decoration:none;overflow-wrap:anywhere}
.rmail:hover{color:var(--ink);text-decoration:underline}
/* NO ADDRESS IS A REAL STATE and it is worth seeing: this person cannot sign in at all.
   Italic and dim - a sentence, not a link, because there is nothing to click. */
.rmail.none{font-style:italic;color:var(--muted)}

/* ---- My integrations panel (integrations.js) ---- */
.tdrow{display:flex;align-items:center;gap:9px;margin:2px 0 8px}
.tddot{width:9px;height:9px;border-radius:50%;flex:none;background:var(--muted)}
.tddot.on{background:var(--green)}
.tddot.warn{background:var(--amber)}
.tddot.off{background:var(--red)}
button.btn.danger{border-color:var(--red-line);color:var(--red)}
button.btn.danger:hover{background:var(--red-wash)}
#integWrap code{font-family:var(--mono);font-size:12px;background:var(--amber-wash);padding:1px 4px;border-radius:3px}
.tdflash{padding:8px 11px;border-radius:5px;font-size:13.5px;margin-bottom:10px;font-weight:500}
.tdflash.ok{background:var(--green-wash);color:var(--green);border:1px solid var(--green-line)}
.tdflash.bad{background:var(--red-wash);color:var(--red);border:1px solid var(--red-line)}

/* The copy-link 🔗 on an opportunity card header (crm.js) */
.oplink{border:0;background:none;cursor:pointer;font-size:14px;line-height:1;padding:2px 5px;
  margin-left:8px;border-radius:4px;opacity:.55;vertical-align:middle}
.oplink:hover{opacity:1;background:var(--soft-2)}
.oplink.ok{opacity:1;color:var(--green)}

/* ---- ACTING FOR AN AGENT (shell.js) ----
   Amit: "make a red banner on top or bottom across the screen telling me who i am acting as."

   BOTTOM, FIXED, FULL WIDTH. The header already carries a 3px red rule and the identity menu;
   a second red band directly under it would read as decoration on the same object. At the foot
   of the window it is somewhere the eye does not expect a permanent fixture, which is exactly
   why it registers - and it is the last thing in view when he presses Issue.

   EVERY COLOUR IS A TOKEN (CLAUDE.md). --red-solid / --on-red-solid are the fill pair that is
   deliberately IDENTICAL in light and dark: this bar means the same thing at midnight as at
   noon, and a warning that dims itself in dark mode is a warning that gets missed. */
#actingBar{position:fixed;left:0;right:0;bottom:0;z-index:120;
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  padding:9px 16px;background:var(--red-solid);color:var(--on-red-solid);
  font-size:13.5px;box-shadow:0 -2px 10px rgba(0,0,0,.18)}
#actingBar .actingWho{font-weight:600}
#actingBar .actingWhy{opacity:.9}
#actingBar .actingStop{margin-left:auto;flex:none;cursor:pointer;
  border:1px solid var(--on-red-solid);background:transparent;color:var(--on-red-solid);
  border-radius:5px;padding:4px 14px;font-size:13px;font-weight:600}
#actingBar .actingStop:hover{background:var(--on-red-solid);color:var(--red-solid)}
/* The bar sits ON the page, so the last row of any scrolled list would hide under it. */
body:has(#actingBar){padding-bottom:52px}

/* The picker */
#actAsWrap{position:fixed;inset:0;z-index:130;background:rgba(0,0,0,.42);
  display:flex;align-items:flex-start;justify-content:center;padding:8vh 16px 24px;
  overflow:auto}
.aaCard{background:var(--bg);border:1px solid var(--line);border-radius:9px;
  max-width:460px;width:100%;padding:18px 20px;box-shadow:0 10px 40px rgba(0,0,0,.28)}
.aaHead{font-size:16px;font-weight:650;margin-bottom:6px}
.aaNote{font-size:13px;color:var(--dim);margin-bottom:14px;line-height:1.45}
.aaGroup{margin-bottom:12px}
.aaFirm{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  margin-bottom:5px}
.aaPerson{display:block;width:100%;text-align:left;cursor:pointer;
  border:1px solid var(--line);background:var(--soft);color:var(--ink);
  border-radius:6px;padding:8px 11px;margin-bottom:5px;font-size:14px}
.aaPerson:hover{border-color:var(--red);background:var(--soft-2)}
.aaTag{font-size:11px;color:var(--muted);margin-left:6px}
.aaNone{font-size:13px;color:var(--muted);font-style:italic}
.aaFoot{margin-top:14px;text-align:right}
.aaCancel{cursor:pointer;border:1px solid var(--line);background:var(--bg);color:var(--ink);
  border-radius:5px;padding:6px 14px;font-size:13px}
