@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap');

/* eeestock — 2026-08-24
   Written from scratch, NOT copied from manage/style.css. That file is ~1150
   lines with eleven competing .stats layers and a :root that declares violet
   then overrides it with cyan further down, so its token names lie about
   their values. Everything below has ONE :root and the names match the hexes.

   Colour rules carried over from the portal/panel (they are correct):
     1. cyan family = structure and brand ONLY, never a status
     2. --good / --warn / --alert = verdict ONLY, never decoration
     3. a decorative tint may not reuse a verdict hex
   Boxes carry no verdict, so their accents come from the 3-tint decorative
   cycle. Amber and red are reserved here for low/zero stock, which IS one. */

:root{
  color-scheme:dark;
  --bg:#15151D; --bg-2:#1B1B25; --surface:#20202B; --surface-2:#282835;
  --line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.18);
  --text:#F5F5F9; --muted:#A9A9BA; --faint:#82828F; --label:#C9C9D8;

  --cyan:#67E8F9; --cyan-ink:#4FD3E8; --cyan-fill:#2FB6CE; --cyan-deep:#1E8FA6;
  --cyan-dim:rgba(103,232,249,.10);

  --good:#5CD6A0; --good-dim:rgba(92,214,160,.12);
  --warn:#E0A458; --warn-dim:rgba(224,164,88,.13);
  --alert:#F26D78; --alert-dim:rgba(242,109,120,.12);

  --display:'Space Grotesk',system-ui,sans-serif;
  --sans:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace;

  --pad:clamp(14px, 1.6vw, 22px);
  /* ⚠️ The content column must GROW when the viewport does. A hard
     --maxw:1240px meant that zooming out — which widens the viewport in CSS
     pixels — left the page pinned at 1240px with dead space either side.
     The 1240px floor keeps the 100% view unchanged; 88vw lets it expand from
     there; 2000px stops it becoming an unreadable band on a huge monitor. */
  --maxw:clamp(1240px, 88vw, 2000px); --r:12px;
}

*{box-sizing:border-box}
/* ⚠️ The `hidden` attribute only gets display:none from the BROWSER default
   stylesheet, so any author rule setting display — .picnow{display:inline-block},
   .picgone{display:flex} — silently outranks it and the element stays visible.
   This is why the thumbnail and its "will be removed" line showed together. */
[hidden]{display:none!important}
html,body{height:100%}
body{
  margin:0;background:var(--bg);color:var(--text);
  font:400 14px/1.5 var(--sans);-webkit-font-smoothing:antialiased;
  min-height:100%;position:relative
}

/* Static lattice underneath everything. The moving device field sits on top
   of this; both are decoration and neither takes pointer events. */
body::before{
  content:"";position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.5;
  background:
    radial-gradient(circle at 18% 12%, rgba(103,232,249,.055), transparent 42%),
    radial-gradient(circle at 82% 78%, rgba(127,180,245,.045), transparent 46%),
    repeating-linear-gradient(60deg,  rgba(255,255,255,.022) 0 1px, transparent 1px 128px),
    repeating-linear-gradient(-60deg, rgba(255,255,255,.022) 0 1px, transparent 1px 128px);
}

/* The drifting Arduinos, Pis, servos and passives. Fixed and full-viewport
   so canvas coordinates equal clientX/clientY with no offset maths. */
#devbg{
  position:fixed;inset:0;width:100%;height:100%;
  z-index:0;pointer-events:none;display:block
}

.wrap{position:relative;z-index:1}

/* ===== CURSOR AURA =========================================================
   Ported from the panel. --mx/--my are set per element by fx.js.
   ⚠️ background-IMAGE, never the `background` shorthand — every target below
   sets background-color, and the shorthand would wipe it and leave the
   element transparent over whatever is behind it.
   ⚠️ NO :focus aura. Focus outlives the cursor, so the gradient freezes at
   the last --mx/--my and reads as stuck. Hover only. */
.ccard,.btn,.crow,.filterbar input,.loginbox input,
.fbody input,.fbody select,.fbody textarea{
  transition:background-image .18s ease,border-color .18s ease,
             transform .14s ease,box-shadow .14s ease
}
.btn:not(:hover):not(.fill),.crow:not(:hover),
.filterbar input:not(:hover),.loginbox input:not(:hover),
.fbody input:not(:hover),.fbody select:not(:hover),
.fbody textarea:not(:hover){background-image:none}

/* ---------- header ------------------------------------------------------ */
/* No border-bottom. The gradient fades to transparent on its own, which
   separates the header without drawing a hard rule across the page — and a
   rule directly under the Sign out button read as an underline on it. */
header{
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  flex-wrap:wrap;row-gap:12px;
  padding:14px var(--pad) 16px;
  background:linear-gradient(180deg,var(--bg-2),rgba(27,27,37,0));
  max-width:var(--maxw);margin:0 auto
}
/* TechSpot 2026-10-06: the logo is a square icon, so .brand CENTRES its items and the logo / name /
   sign-in logo nudges are 0. The icam notes below describe the OLD wordmark lockup. */
/* ⚠️ align-items:BASELINE, not center. The logo is cropped tight to its ink,
   so the bottom edge of the image IS the "icam" baseline (the wordmark has no
   descenders). A flex item's baseline for a replaced element is its bottom
   margin edge — so baseline alignment sits "icam" and "EEE Stock" on the same
   line of type. Centring instead floats the wordmark below the text. */
.brand{display:flex;align-items:center;gap:20px;min-width:0;flex:1 1 auto;
  flex-wrap:wrap;row-gap:12px}
/* The Icam lockup is 1.85:1 (mark + wordmark), not a square glyph — height
   is what to control, width follows. Sized from a 222x120 source so it stays
   sharp on a 3x display. */
.logo{
  height:40px;width:auto;display:block;flex-shrink:0;
  /* Optical nudge. Baseline alignment puts the image's bottom edge on the
     text baseline, but the alpha floor used when keying the white out trims
     the faintest anti-aliased row of "icam", so the mark reads ~1px high.
     ★ This is the ONE number to change if it still looks off. */
  position:relative;top:var(--logo-nudge, 0px)
}
/* One line, not a stack. */
.btxt{display:flex;flex-direction:row;align-items:baseline;gap:0;min-width:0}
.brand b{
  font:700 17px var(--display);letter-spacing:.02em;color:var(--text);
  white-space:nowrap
}
/* Same size as "EEE Stock" (17px). Tracking drops .16em -> .07em: spacing
   that opens up 10px caps nicely reads as stretched at 17px, because tracking
   is proportional to the glyphs it is separating. Line-height is pinned so
   the divider rule beside it stays close to the cap height instead of
   inheriting a tall `normal` line box. */
.brand i{
  font-style:normal;font:700 17px/1.15 var(--display);letter-spacing:.02em;
  text-transform:none;color:var(--cyan-ink);opacity:.9;
  margin-left:14px;padding-left:14px;border-left:1px solid var(--line-2);
  white-space:nowrap
}
.who{display:flex;align-items:center;gap:12px;font-size:12.5px;color:var(--muted);
  flex-shrink:0}
/* ⚠️ Three buttons now (Shopping list arrived in §54.5), and on a phone the
   row is wider than the screen. A single overflowing row makes Android zoom
   the whole page out to fit it — the "free space on the right" look. Below
   900px the row takes the full width and wraps instead:
       [New section] [Shopping list]
       [Sign out] */
@media (max-width:900px){
  .who{flex:1 1 100%;flex-wrap:wrap;flex-shrink:1;row-gap:10px}
}
/* Belt and braces: nothing on this page is meant to scroll sideways, so any
   future overflow gets clipped instead of zooming the page out. clip, not
   hidden — hidden would turn body into a scroll container. */
html,body{overflow-x:clip}

/* ---------- nameplate ---------------------------------------------------
   ⚠️ The plate lives INSIDE .brand, and that is what aligns it.
   .brand is align-items:baseline, so dropping the plate in as a third child
   puts its baseline on the same line as "EEE Stock" for free — no magic
   offset to maintain. Centring it in the header could never line up: the
   header centres its children vertically, while "EEE Stock" sits low against
   the 40px logo because of that same baseline alignment. The two were ~14px
   apart, and no nudge would have survived a font or logo size change.

   margin-left:auto pushes it to the right end of the brand row, which is
   flex:1, so it settles in the open space just left of the buttons. */
.nameplate{
  margin-left:auto;margin-right:34px;pointer-events:none;
  /* Optical nudge so the NAME reads level with "EEE Stock". Baseline
     alignment is geometrically exact, but the name is 15px against the
     wordmark's 17px, and the smaller face sits visually high on a shared
     baseline. Moves the text and its rule together.
     ★ This is the ONE number to change if it is still off. */
  position:relative;top:var(--name-nudge, 0px)
}
.npn{
  position:relative;display:inline-block;
  font:600 15px/1.2 var(--display);letter-spacing:.015em;
  color:var(--text);white-space:nowrap
}
/* Rule is the width of the NAME, not its container, so it stays correct when
   the plate wraps to a line of its own on a phone. */
/* ⚠️ 1.5px, not 1px. Browser zoom scales CSS pixels to device pixels, so at
   67% a 1px rule is 0.67 device px — it rounds toward zero and vanishes, and
   the semi-transparent gradient dims it further before it gets there. 1.5px
   still lands on a full device pixel at 67% and reads the same at 100%. */
.npn::after{
  content:"";position:absolute;left:0;right:0;bottom:-7px;height:1.5px;
  background:linear-gradient(90deg,transparent,rgba(103,232,249,.62),transparent)
}
/* Sign out is the only control that ends the session, so it warms to the
   alert hue on hover rather than the cyan every other control uses. */
.mini.out:hover{
  background-color:var(--alert-dim);border-color:rgba(242,109,120,.4);
  color:var(--alert)
}

/* ---------- drag handle -------------------------------------------------
   ⚠️ touch-action:none is mandatory. Without it the browser claims the
   gesture for scrolling and pointermove stops firing mid-drag on any
   touchscreen — the section detaches and then freezes. */
.ghandle{
  appearance:none;background:none;border:0;padding:2px;margin-left:-2px;
  color:var(--faint);cursor:grab;touch-action:none;border-radius:6px;
  display:inline-flex;align-items:center;flex-shrink:0;
  transition:color .13s ease,background-color .13s ease
}
.ghandle .ic{width:17px;height:17px}
.ghandle:hover{color:var(--cyan-ink);background-color:var(--cyan-dim)}
.ghandle:active{cursor:grabbing}
.sgroup{transition:box-shadow .15s ease}
.sgroup.dragging{
  position:relative;z-index:40;cursor:grabbing;
  background:rgba(32,32,43,.86);border-radius:12px;
  box-shadow:0 18px 46px rgba(0,0,0,.5);
  outline:1px solid rgba(103,232,249,.28);
  transition:none
}
/* Text selection during a drag turns the whole page blue. */
body.dragging-on{user-select:none;-webkit-user-select:none}
body.dragging-on .ccard{pointer-events:none}

/* ---------- section header (the hairline rule) -------------------------- */
.sgroup{max-width:var(--maxw);margin:26px auto 16px;padding:0 var(--pad)}
/* ---------- section header (the hairline rule) --------------------------
   ⚠️ The rule is a real <span class="grule">, NOT ::after. A pseudo-element
   is always the last child, so the action buttons could never sit after it. */
.glabel{display:flex;align-items:center;gap:12px;margin:0 0 14px;
  flex-wrap:wrap;row-gap:9px}
.gtick{
  width:3px;height:18px;border-radius:2px;flex-shrink:0;
  background:linear-gradient(180deg,var(--cyan),var(--cyan-deep))
}
/* Section names are user-editable now, so this is a real heading in the
   display face — NOT the panel's 10.5px mono caps. Seven of these are the
   primary structure of the page, and forcing uppercase would misrepresent
   whatever the operator actually typed. */
.gname{
  font:600 17px/1.2 var(--display);letter-spacing:.005em;color:var(--text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  max-width:100%;min-width:0;flex:0 1 auto
}
.grule{
  flex:1;height:2px;min-width:16px;border-radius:1px;
  background:linear-gradient(90deg,rgba(103,232,249,.45),rgba(103,232,249,.04))
}
.gacts{display:flex;align-items:center;gap:6px;flex-shrink:0}
.gacts form{display:inline;margin:0}

/* ---------- .mini — ported from the panel's subscriber row actions -------
   Filled tinted pills with an icon and a title tooltip, as manage/style.css
   line 329. `good` = create, `del` = destroy, plain = edit. Deliberately NOT
   in the cursor-aura set: these carry their own hover fill, as the panel's do. */
.mini{
  display:inline-flex;align-items:center;gap:6px;padding:6px 11px;min-height:30px;
  font:500 12.5px var(--sans);background-color:var(--surface-2);color:var(--label);
  border:1px solid transparent;border-radius:8px;cursor:pointer;
  text-decoration:none;
  transition:background-color .18s ease,border-color .18s ease,color .18s ease
}
.mini .ic{width:15px;height:15px;flex-shrink:0}
.mini:hover{background-color:rgba(103,232,249,.13);border-color:rgba(103,232,249,.45);color:var(--text)}
.mini:focus-visible{outline:2px solid var(--cyan-deep);outline-offset:1px}
.mini.good{background-color:var(--cyan-dim);color:var(--cyan-ink);border-color:rgba(103,232,249,.3)}
.mini.good:hover{background-color:rgba(103,232,249,.17);border-color:rgba(103,232,249,.55);color:var(--cyan)}
.mini.del{background-color:var(--alert-dim);color:var(--alert);border-color:rgba(242,109,120,.26)}
.mini.del:hover{background-color:rgba(242,109,120,.2);border-color:rgba(242,109,120,.55);color:var(--alert)}
/* Labels collapse to icon-only when space is tight; the title attribute
   still names the action, and touch users get a squarer target. */
@media (max-width:920px){
  .mini .mtxt{display:none}
  .mini{padding:8px 9px}
}

/* ⚠️ Bottom space is PADDING, not margin. As the last element inside .wrap,
   a bottom margin collapses straight through .wrap and body and produces no
   visible gap at all — the button ends up flush against the viewport edge. */
.addsec{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  max-width:var(--maxw);margin:26px auto 0;padding:0 var(--pad) 80px
}

/* ---------- box cards ----------------------------------------------------
   Anatomy lifted from manage/style.css section 14 ("STAT CARDS - final").
   ★ The panel's own comment is the whole point: "the tile carries the colour,
   so the number stays white". A SOLID saturated tile gives the hue a shape
   with weight. Tinting thin text over a faint wash — which is what this was
   doing — has nothing to carry the colour and reads washed out no matter
   which palette you put in it.

   Colour cycles PER BOX across the whole page, so three different hues sit
   side by side in every row, exactly like the panel's Tiers strip. */
.closets{
  display:grid;gap:12px;
  grid-template-columns:repeat(auto-fill,minmax(min(248px,100%),1fr))
}
.ccard{
  appearance:none;-webkit-appearance:none;text-align:left;cursor:pointer;
  display:flex;align-items:center;gap:14px;
  padding:15px 16px;border-radius:14px;
  background-color:var(--surface);border:1px solid var(--line);color:var(--text);
  font-family:var(--sans);
  transition:border-color .25s ease,transform .25s cubic-bezier(.16,1,.3,1),
             box-shadow .25s ease,background-image .18s ease
}

/* Solid tile plus its lifted label partner. The tile is dark enough to carry
   a white glyph; the label is the same hue raised for a dark surface. None
   collides with --good/--warn/--alert, so amber and red still mean stock. */
.ccard.k1{--tile:#1E9CB0;--tint:#67E8F9;--tint-glow:rgba(103,232,249,.26)}
.ccard.k2{--tile:#4C74C9;--tint:#7FA0E8;--tint-glow:rgba(127,160,232,.26)}
.ccard.k3{--tile:#7A5FC4;--tint:#A98BE0;--tint-glow:rgba(169,139,224,.26)}
.ccard.k4{--tile:#2C9E8B;--tint:#5FC4B0;--tint-glow:rgba(95,196,176,.26)}
.ccard.k5{--tile:#AE5B94;--tint:#D98BC4;--tint-glow:rgba(217,139,196,.26)}

.si{
  display:grid;place-items:center;width:42px;height:42px;border-radius:12px;
  flex:none;border:0;background:var(--tile,#1E9CB0);color:#fff
}
.si .ic{width:20px;height:20px;stroke-width:1.9}

.cbody{display:flex;flex-direction:column;min-width:0;flex:1;gap:5px}
/* The box NAME is the primary element, so it is the largest thing in the
   card and sits first. Inverting the sizes without also swapping the order
   leaves a big title underneath a small figure, which reads as a caption in
   the wrong place. Name 18px > number 15px. */
.ccard .cname{
  font:600 18px/1.15 var(--display);letter-spacing:-.01em;
  color:var(--tint,#67E8F9);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap
}
.ccard b{
  display:flex;align-items:baseline;gap:5px;
  font:600 15px/1 var(--display);letter-spacing:-.02em;color:var(--text)
}
/* the unit keeps the smaller figure unambiguous against the corner count */
.ccard b em{
  font:700 9.5px var(--mono);font-style:normal;text-transform:uppercase;
  letter-spacing:.1em;color:var(--faint)
}
/* ⚠️ NOT absolutely positioned. The panel's .tol is, and it collided with
   longer labels at every width until it was rewritten (§15). Kept in flow. */
.tol{
  flex:none;align-self:flex-start;font-style:normal;
  font:400 10.5px var(--mono);color:var(--faint);white-space:nowrap
}
.ccard .tol.zero{color:var(--alert)}
.ccard .tol.low{color:var(--warn)}

.ccard:hover{
  transform:translateY(-2px);border-color:var(--line-2);
  box-shadow:0 18px 40px -24px rgba(0,0,0,.7);
  background-image:radial-gradient(300px circle at var(--mx,50%) var(--my,50%),
    var(--tint-glow,rgba(103,232,249,.26)),transparent 66%)
}
.ccard:focus-visible{outline:2px solid var(--tint,#67E8F9);outline-offset:2px}

/* ---------- buttons ----------------------------------------------------- */
/* Geometry lifted from manage/style.css line 436 so buttons on both
   products are the same object. */
.btn{
  display:inline-flex;align-items:center;gap:7px;
  appearance:none;cursor:pointer;border-radius:9px;font:500 13.5px var(--sans);
  padding:9px 16px;border:1px solid var(--line-2);background-color:transparent;
  color:var(--label);text-decoration:none;white-space:nowrap
}
.btn .ic{width:16px;height:16px;flex-shrink:0}
.btn:hover{
  color:var(--text);
  background-image:radial-gradient(150px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.20),transparent 66%)
}
/* .fill already uses background-image for its gradient, so the aura has to
   be LAYERED above it rather than replacing it. */
/* Darker at rest, brighter on hover — the login button's behaviour, now the
   behaviour of every filled button.
   ⚠️ The cursor-following white aura that used to be here is deliberately
   gone. It was a different interaction from the login button's plain
   brighten, and two filled buttons that respond differently to the same
   gesture read as a bug rather than as variety. */
.btn.fill{
  border-color:transparent;color:#06222A;font-weight:700;
  background-color:#29ABC3;
  background-image:linear-gradient(180deg,#3EC6DD,#29ABC3);
  transition:background-image .18s ease,background-color .18s ease,
             box-shadow .18s ease
}
.btn.fill:hover{
  color:#06222A;
  background-color:var(--cyan-fill);
  background-image:linear-gradient(180deg,var(--cyan),var(--cyan-fill));
  box-shadow:0 0 26px -9px rgba(103,232,249,.6)
}
/* Sign out sits beside "New section" in both places, so it is a full .btn,
   not a .mini pill — a smaller neighbour read as subordinate rather than
   as the other half of a pair. */
.btn.ghost{background-color:var(--surface);border-color:var(--line-2);color:var(--label)}
.btn.ghost:hover{
  color:var(--alert);border-color:rgba(242,109,120,.45);
  background-image:radial-gradient(150px circle at var(--mx,50%) var(--my,50%),
    rgba(242,109,120,.20),transparent 66%)
}
.btn.danger{border-color:rgba(242,109,120,.34);color:var(--alert)}
.btn.danger:hover{
  color:var(--alert);
  background-image:radial-gradient(150px circle at var(--mx,50%) var(--my,50%),
    rgba(242,109,120,.22),transparent 66%)
}

/* ---------- modal ------------------------------------------------------- */
.modal{
  position:fixed;inset:0;z-index:60;display:flex;align-items:flex-start;
  justify-content:center;padding:24px 14px;overflow-y:auto;
  background:rgba(8,8,13,.72);backdrop-filter:blur(3px)
}
.modal[hidden]{display:none}
/* Dialogs are sized to their CONTENT, not to one blanket maximum. A single
   text field stretched across 880px reads as a mistake — the eye travels the
   whole width and finds nothing there.
     .sheet         box contents — rows with a thumbnail, name, qty, actions
     .sheet.form    component add/edit — a two-column field grid
     .sheet.narrow  one field and a Save button
   Order matters: .narrow and .form are equal specificity, so .narrow must
   come last to win on the name dialog, which carries both. */
.sheet{
  width:100%;max-width:980px;margin:auto;background:var(--bg-2);
  border:1px solid var(--line-2);border-radius:14px;
  box-shadow:0 26px 70px rgba(0,0,0,.6);overflow:hidden
}
.sheet.form{max-width:620px}
.sheet.narrow{max-width:430px}
.sheethead{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  flex-wrap:wrap;row-gap:10px;
  padding:15px 18px;border-bottom:1px solid var(--line);
  background:linear-gradient(180deg,var(--surface),var(--bg-2))
}
.sheethead .t b{display:block;font:600 16px var(--display)}
.sheethead .t span{
  display:block;margin-top:3px;font:400 11px var(--mono);color:var(--cyan-ink);opacity:.75
}
.hact{display:flex;align-items:center;gap:8px;flex-shrink:0;flex-wrap:wrap}
.hact form{display:inline;margin:0}

.filterbar{padding:12px 18px 0}
.filterbar input{
  width:100%;padding:9px 12px;border-radius:9px;background-color:var(--bg);
  border:1px solid var(--line);color:var(--text);font:400 13px var(--sans)
}
.filterbar input:hover{
  border-color:rgba(103,232,249,.4);
  background-image:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.18),transparent 66%)
}
.filterbar input:focus{outline:none;border-color:var(--cyan-deep)}

.rows{padding:12px 18px 18px;display:flex;flex-direction:column;gap:8px}
.crow{
  display:flex;align-items:center;gap:13px;padding:10px 12px;
  background-color:var(--surface);border:1px solid var(--line);border-radius:10px
}
.crow:hover{
  border-color:var(--line-2);
  background-image:radial-gradient(300px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.10),transparent 70%)
}
.crow[hidden]{display:none}
.thumb{
  width:52px;height:52px;flex-shrink:0;border-radius:8px;object-fit:cover;
  background:var(--surface-2);border:1px solid var(--line)
}
.thumb.ph{
  display:flex;align-items:center;justify-content:center;
  font:700 9px var(--mono);letter-spacing:.1em;color:var(--faint)
}
.cmain{min-width:0;flex:1}
.cmain b{display:block;font:600 13.5px var(--sans);color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cmeta{
  display:flex;flex-wrap:wrap;gap:4px 12px;margin-top:3px;
  font:400 11px var(--mono);color:var(--faint)
}
.cmeta a{color:var(--cyan-ink);text-decoration:none}
.cmeta a:hover{text-decoration:underline}
.qty{
  flex-shrink:0;min-width:52px;text-align:right;
  font:600 16px var(--display);color:var(--text)
}
.qty.zero{color:var(--alert)}
.qty.low{color:var(--warn)}
.racts{display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap}

/* ---- shopping-list controls -------------------------------------------
   Two identical compound controls: a number field welded to an action
   button. Built as one bordered shell with a divider so the pair reads as a
   single control rather than a stray input next to a stray button. */
.incart{
  display:inline-flex;align-items:center;height:30px;padding:0 11px;
  border-radius:8px;white-space:nowrap;
  font:700 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;
  background:var(--warn-dim);color:var(--warn);border:1px solid rgba(224,164,88,.32)
}

/* One bordered shell holding a number field and its action button, so the
   pair reads as a single control. Height matched to .mini so everything in
   the row sits on one line. */
.qbox{
  display:inline-flex;align-items:stretch;height:30px;margin:0;overflow:hidden;
  border:1px solid var(--line-2);border-radius:8px;background:var(--bg);
  transition:border-color .14s ease,box-shadow .14s ease
}
/* Each carries its own hue AT REST, not only on hover — two identical grey
   boxes side by side would need reading before every click. Cyan = an action
   (order it), green = a verdict (it arrived); both stay clear of the
   decorative palette, per the colour rules. */
.qbox.qcart{border-color:rgba(103,232,249,.30)}
.qbox.qrecv{border-color:rgba(92,214,160,.30)}
.qbox.qcart:focus-within{box-shadow:0 0 0 3px rgba(103,232,249,.14)}
.qbox.qrecv:focus-within{box-shadow:0 0 0 3px rgba(92,214,160,.14)}

.qnum{
  width:46px;padding:0 4px;text-align:center;border:0;background:transparent;
  color:var(--text);font:600 13px var(--sans);appearance:textfield
}
/* the spinners make a 46px field unusable */
.qnum::-webkit-outer-spin-button,
.qnum::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.qnum:focus{outline:none}
.qnum::placeholder{color:var(--faint);font-weight:400}

.qact{
  display:grid;place-items:center;width:36px;padding:0;cursor:pointer;border:0;
  transition:background-color .14s ease,color .14s ease
}
.qact .ic{width:15px;height:15px}
.qact.cart{
  background:var(--cyan-dim);color:var(--cyan-ink);
  border-left:1px solid rgba(103,232,249,.28)
}
.qact.cart:hover{background:rgba(103,232,249,.24);color:var(--cyan)}
.qact.recv{
  background:var(--good-dim);color:var(--good);
  border-left:1px solid rgba(92,214,160,.28)
}
.qact.recv:hover{background:rgba(92,214,160,.24);color:#7BE2B6}
.racts form{margin:0}
.note{
  margin-top:4px;font:400 11.5px var(--sans);color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap
}
.empty{padding:26px 4px;text-align:center;color:var(--faint);font-size:13px}

/* ---------- public shopping list ---------------------------------------- */
/* Reuses .sheet and .crow so the page looks like a box popup's contents
   without a popup around it. */
.cartsheet{max-width:none;margin:0}
.needqty{
  display:flex;flex-direction:column;align-items:flex-end;gap:3px;
  flex:none;min-width:64px
}
.needqty b{font:600 22px/1 var(--display);letter-spacing:-.02em;color:var(--warn)}
.needqty em{
  font:700 9px var(--mono);font-style:normal;letter-spacing:.12em;
  text-transform:uppercase;color:var(--faint)
}
.cmeta .nolink{color:var(--alert);opacity:.85}
.cartfoot{
  margin:0;max-width:640px;
  font:400 13px/1.6 var(--sans);color:var(--muted)
}

/* ---------- form -------------------------------------------------------- */
.form{display:flex;flex-direction:column}
.fbody{padding:16px 18px;display:grid;gap:13px;grid-template-columns:1fr 1fr}
.fbody label{display:flex;flex-direction:column;gap:5px;min-width:0}
.fbody label.wide{grid-column:1/-1}
.fbody span.lb,.loginbox span.lb{
  font:700 10px var(--mono);letter-spacing:.13em;text-transform:uppercase;
  color:var(--faint)
}
/* ⚠️ Checkboxes are EXCLUDED. width:100% on a checkbox makes it stretch as a
   flex item — it collapses to a squashed grey square and shoves its label to
   the far right of the row. Every :not([type=checkbox]) below is load-bearing. */
.fbody input:not([type=checkbox]),.fbody select,.fbody textarea{
  width:100%;padding:9px 11px;border-radius:9px;background-color:var(--bg);
  border:1px solid var(--line);color:var(--text);font:400 13px var(--sans)
}
.fbody textarea{resize:vertical;min-height:62px}
.fbody input:not([type=checkbox]):hover,.fbody select:hover,.fbody textarea:hover{
  border-color:rgba(103,232,249,.4);
  background-image:radial-gradient(180px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.18),transparent 66%)
}
.fbody input:not([type=checkbox]):focus,.fbody select:focus,.fbody textarea:focus{
  outline:none;border-color:var(--cyan-deep)
}
/* The native "Choose File" button is a grey system control and looked
   pasted in next to everything else. */
.fbody input[type=file]{padding:8px 10px;font-size:12px;color:var(--muted);cursor:pointer}
.fbody input[type=file]::file-selector-button{
  margin-right:11px;height:30px;padding:0 13px;border-radius:7px;cursor:pointer;
  border:1px solid var(--line-2);background:var(--surface-2);color:var(--label);
  font:600 12px var(--sans);
  transition:background-color .15s ease,border-color .15s ease,color .15s ease
}
.fbody input[type=file]::file-selector-button:hover{
  background:rgba(103,232,249,.13);border-color:rgba(103,232,249,.45);color:var(--text)
}

/* ---- current picture, with an × on it -------------------------------- */
.fbody .fpic{display:flex;flex-direction:column;gap:8px;grid-column:1/-1;min-width:0}
.picrow{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
/* Match the file picker's box to the 78px thumbnail — only while a thumbnail
   is actually there, so an empty picker stays a normal-height field.
   ⚠️ Sized with the box model (23 + 30 + 23 + 2px borders = 78), not with
   flex centring: input[type=file] renders through a shadow tree and flex
   alignment on it is not dependable across browsers. */
.picrow:has(.picnow:not([hidden])) input[type=file]{
  padding:23px 12px;min-height:78px
}
.piccol{display:flex;flex-direction:column;gap:8px;min-width:0;flex:1 1 200px}
.picnow{position:relative;display:inline-block;line-height:0;flex:none}
.picnow img{
  width:78px;height:78px;object-fit:cover;border-radius:10px;display:block;
  border:1px solid var(--line);background:var(--surface-2);cursor:zoom-in;
  transition:border-color .14s ease
}
.picnow img:hover{border-color:rgba(103,232,249,.5)}
.thumb[data-zoom]{cursor:zoom-in;transition:border-color .14s ease}
.thumb[data-zoom]:hover{border-color:rgba(103,232,249,.5)}

/* ---- full-size viewer ------------------------------------------------- */
.lightbox{
  position:fixed;inset:0;z-index:90;display:flex;align-items:center;
  justify-content:center;padding:44px 24px;cursor:zoom-out;
  background:rgba(6,6,10,.88);backdrop-filter:blur(5px)
}
.lightbox img{
  max-width:min(1040px,100%);max-height:100%;object-fit:contain;
  border-radius:12px;border:1px solid var(--line-2);
  box-shadow:0 34px 100px rgba(0,0,0,.75);cursor:default
}
.lbx{
  position:absolute;top:20px;right:22px;width:40px;height:40px;padding:0;
  display:grid;place-items:center;border-radius:50%;cursor:pointer;
  background:var(--surface);border:1px solid var(--line-2);color:var(--label);
  transition:background-color .14s ease,border-color .14s ease,color .14s ease
}
.lbx .ic{width:18px;height:18px;stroke-width:2.2}
.lbx:hover{background:rgba(242,109,120,.18);border-color:rgba(242,109,120,.5);color:var(--alert)}
.picx{
  position:absolute;top:-8px;right:-8px;width:24px;height:24px;padding:0;
  display:grid;place-items:center;border-radius:50%;cursor:pointer;
  background:var(--alert);color:#2A0509;border:2px solid var(--bg-2);
  transition:transform .13s ease,filter .13s ease
}
.picx .ic{width:12px;height:12px;stroke-width:3}
.picx:hover{transform:scale(1.09);filter:brightness(1.08)}
.picgone{
  margin:0;display:flex;align-items:center;gap:9px;
  font:400 12.5px var(--sans);color:var(--muted)
}
.undo{
  appearance:none;background:none;border:0;padding:0;cursor:pointer;
  font:600 12.5px var(--sans);color:var(--cyan-ink);text-decoration:underline
}
.undo:hover{color:var(--cyan)}
.formfoot{
  display:flex;justify-content:flex-end;gap:8px;padding:14px 18px;
  border-top:1px solid var(--line);background:var(--surface)
}

/* ---------- scroll progress ---------------------------------------------
   Ported from culmensoft.com (assets/culmen-polish.css). Two things in it are
   load-bearing:

   1. transform:scaleX, NOT width. Animating width forces a LAYOUT on every
      scroll frame; scaleX is compositor-only, so the GPU moves it without
      touching layout or paint — exactly when the browser is busiest.
      ⚠️ app.js must set bar.style.transform, never bar.style.width. Change
      one without the other and the bar stays invisible at scale 0.

   2. z-index:9999. Its natural place in this stack is above .wrap (1) but the
      bar must also clear the modals (60) and the image viewer (90); at any
      lower value it slides under them and looks like it has disappeared.

   3px rather than culmensoft's 2px — on a phone at reduced zoom a 2px bar is
   most of the way to vanishing (same sub-pixel rounding as the nameplate
   hairline). */
.progress{
  position:fixed;top:0;left:0;width:100%;height:3px;z-index:9999;
  transform:scaleX(0);transform-origin:0 50%;will-change:transform;
  background:linear-gradient(90deg,var(--cyan-fill),var(--cyan));
  box-shadow:0 0 14px rgba(103,232,249,.55);
  transition:transform .1s linear;pointer-events:none
}

/* ---------- flash ------------------------------------------------------- */
/* ---------- flash ------------------------------------------------------
   ⚠️ ALIGNMENT. .sgroup is `max-width:var(--maxw)` with `padding:0 var(--pad)`,
   so its CONTENT spans maxw minus twice the pad. A flash set to
   `max-width:var(--maxw)` is a box that is itself maxw wide, i.e. 2×pad wider
   than everything it sits above, and it overhangs on both sides. The cap has
   to have the padding subtracted from it, not added around it. */
.flash{
  display:flex;align-items:center;gap:12px;
  max-width:calc(var(--maxw) - var(--pad) * 2);
  width:calc(100% - var(--pad) * 2);
  margin:16px auto 0;padding:11px 12px 11px 15px;
  border-radius:9px;font-size:13px;position:relative;z-index:1;
  transition:opacity .18s ease,transform .18s ease
}
.flash > span{flex:1;min-width:0}
.flash.gone{opacity:0;transform:translateY(-6px)}
.flash .fx{
  appearance:none;background:none;border:0;cursor:pointer;padding:4px;
  border-radius:6px;color:inherit;opacity:.6;flex-shrink:0;
  display:inline-flex;align-items:center;
  transition:opacity .13s ease,background-color .13s ease
}
.flash .fx .ic{width:15px;height:15px}
.flash .fx:hover{opacity:1;background-color:rgba(255,255,255,.1)}
.flash .fx:focus-visible{opacity:1;outline:2px solid currentColor;outline-offset:1px}
.flash.err{background:var(--alert-dim);border:1px solid rgba(242,109,120,.3);color:#FCA5A5}
.flash.ok{background:var(--good-dim);border:1px solid rgba(92,214,160,.3);color:#A7F3D0}

/* ---------- login -------------------------------------------------------
   Rebuilt: the previous card was a flat rectangle with cramped labels and a
   full-width button whose label sat hard against the left edge — .btn is
   inline-flex, and without justify-content the text never centres. */
.loginwrap{
  min-height:100dvh;display:flex;align-items:center;justify-content:center;
  padding:30px 18px;position:relative;z-index:1
}
.loginbox{
  position:relative;width:100%;max-width:404px;
  background:linear-gradient(180deg,var(--bg-2),var(--bg));
  /* ⚠️ SOLID, not a gradient. A linear gradient runs corner to corner, so it
     is bright at two corners and fades through the middle of every edge —
     there is no linear gradient that lights all four sides evenly. A flat
     colour is the only thing that does. 1.5px survives zooming out. */
  border:1.5px solid rgba(103,232,249,.55);
  border-radius:18px;
  padding:38px 34px 28px;
  /* Halo hugging the border, wide aura, then the shadow that seats the card. */
  box-shadow:
    0 0 0 4px rgba(103,232,249,.07),
    0 0 78px -14px rgba(103,232,249,.42),
    0 34px 90px rgba(0,0,0,.60)
}

.lgbrand{display:flex;flex-direction:column;align-items:center;text-align:center}
.lglogo{
  height:80px;width:auto;display:block;
  /* Optical centring, measured rather than guessed. Two reference points on
     the 148x80 rendered lockup:
       -1.3px  centres the alpha-weighted INK MASS
      -14.3px  centres the "icam" WORDMARK alone
     The mark and dot float above-left and carry real weight, but the eye
     compares word to word against "EEE Stock" underneath, so neither extreme
     is right. -7px sits between them.
     ★ This is the ONE number to change. More negative = further left. */
  position:relative;left:var(--lglogo-nudge, 0px)
}
/* TechSpot 2026-10-06: 32px, so the gap under the logo equals the gap above it (the PNG carries
   ~5px padding top and bottom, and the title line box adds ~4px). Old icam note below. */
/* Slightly MORE than .loginbox's 38px padding-top, by eye. The image is
   cropped tight to its ink — no internal whitespace — so this margin is the
   whole visible gap, with nothing hidden inside the PNG padding it out. */
.lgbrand b{
  margin-top:32px;font:700 21px/1 var(--display);letter-spacing:.01em;
  color:var(--text)
}
.lgbrand span{
  margin-top:9px;font:700 11px/1 var(--mono);letter-spacing:.2em;
  text-transform:uppercase;color:var(--cyan-ink);opacity:.9
}

.lgerr{
  display:flex;align-items:flex-start;gap:9px;margin:24px 0 -2px;
  padding:11px 13px;border-radius:10px;font-size:12.5px;line-height:1.45;
  background:var(--alert-dim);border:1px solid rgba(242,109,120,.32);color:#FCA5A5
}
.lgerr .ic{width:15px;height:15px;flex:none;margin-top:1px}

.lgf{display:block;margin-top:22px}
.lgl{
  display:block;margin-bottom:8px;
  font:700 10.5px var(--mono);letter-spacing:.15em;text-transform:uppercase;
  color:var(--faint)
}
.loginbox input:not([type=hidden]){
  width:100%;padding:12px 14px;border-radius:10px;
  background-color:var(--surface);border:1px solid var(--line);
  color:var(--text);font:400 14.5px var(--sans);
  transition:border-color .16s ease,box-shadow .16s ease,
             background-color .16s ease,background-image .18s ease
}
.loginbox input:hover{
  border-color:rgba(103,232,249,.34);
  background-image:radial-gradient(200px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.14),transparent 68%)
}
.loginbox input:focus{
  outline:none;border-color:var(--cyan-deep);
  box-shadow:0 0 0 3px rgba(103,232,249,.14)
}
/* ⚠️ Chrome paints its own pale-yellow fill over autofilled fields, which is
   unreadable on a dark form and cannot be overridden with background-color.
   A large inset shadow is the only thing that covers it. */
.loginbox input:-webkit-autofill,
.loginbox input:-webkit-autofill:hover,
.loginbox input:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--text);
  -webkit-box-shadow:0 0 0 40px var(--surface) inset;
  caret-color:var(--text)
}

.lgbtn{
  width:100%;margin-top:26px;padding:13px;font-size:14px;
  justify-content:center     /* .btn is inline-flex — without this the label sits left */
}

.lgfoot{
  position:relative;margin:26px 0 0;padding-top:19px;text-align:center;
  font:600 14px var(--display);color:var(--muted);letter-spacing:.01em
}
.lgfoot::before{
  content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:70%;height:1.5px;
  background:linear-gradient(90deg,transparent,rgba(103,232,249,.55),transparent)
}

/* ---------- responsive -------------------------------------------------- */
/* Three elements in the header now. The subtitle is the first thing to go. */
@media (max-width:1120px){
  .brand i{display:none}
}
/* Below this the header is too tight for a centred third element, so the
   plate leaves the absolute layer and takes a row of its own. */
/* Below this the brand row is too tight for a third element, so the plate
   wraps onto a line of its own beneath the wordmark. */
@media (max-width:900px){
  /* Mobile brand block, three rows:
        [logo] ................. Dr. Fatima Hussein
        EEE Stock
        [New section] [Sign out]
     The name fills the empty space beside the logo and baselines with it;
     the wordmark drops to its own line starting at the logo's left edge.
     ⚠️ Done with `order`, not by moving the markup — the DOM order is what
     desktop needs (logo, wordmark, name) and only the phone differs. */
  .brand{row-gap:14px}
  .logo{order:1}
  /* Sits level with the logo rather than on its baseline. On desktop the
     baseline is shared with "EEE Stock"; here the wordmark is on the next
     line, so baselining the name against the logo's bottom edge just parked
     it low. */
  .nameplate{
    order:2;margin:0 0 0 auto;flex-basis:auto;align-self:center;
    /* align-self:center centres against the whole 40px lockup, i.e. y=20 —
       but the lockup is swoosh on top and "icam" below, so its centre falls
       in empty space and the name flew up. Reference points, in display px:
         20.0  centre of the lockup   (too high)
         31.8  centre of the "icam" wordmark
         40.0  the logo's baseline    (too low)
       +10px lands on 30 — text against text.
       ★ This is the ONE number to change. Larger = lower. */
    top:var(--name-nudge-m, 0px)
  }

  /* flex-basis:100% is what forces the line break — an item that cannot fit
     beside the logo wraps, so no spacer element is needed.
     ⚠️ The indent is padding, not margin: with border-box the padding sits
     INSIDE the 100% basis, so the row cannot overflow.
     13px is measured, not eyeballed — the logo PNG is cropped to its ink and
     the leftmost ink is the swoosh tip, which starts 39 of 222 image px left
     of the "icam" wordmark. At height:40px that is 13 display px. */
  .btxt{order:3;flex-basis:100%;padding-left:var(--btxt-indent, 6px)}
}
@media (max-width:820px){
  .closets{grid-template-columns:repeat(auto-fill,minmax(min(220px,100%),1fr))}
  .fbody{grid-template-columns:1fr}

}
@media (max-width:560px){
  /* The row reflows rather than squeezing: the action buttons drop to their
     own line instead of shrinking the name to three characters. */
  .crow{flex-wrap:wrap;row-gap:9px}
  .cmain{flex:1 1 auto;order:2}
  .thumb{order:1}
  .qty{order:3;min-width:auto}
  .racts{order:4;width:100%;justify-content:flex-end;
    border-top:1px solid var(--line);padding-top:9px}
}
@media (prefers-reduced-motion:reduce){
  *{transition:none!important}
  .ccard:hover{transform:none}
}

/* ---------- scoped search (2026-08-27) -----------------------------------
   Two fields, two scopes, and the scope is written on each one in the mono
   eyebrow so nobody has to guess which box searches what. Left finds
   COMPONENTS but only inside the closets; right finds CLASSES and BOXES
   and never looks at components. Same input geometry as the per-box
   filter, on the page surface instead of inside a sheet. */
.searchbar{
  max-width:var(--maxw);margin:6px auto 0;padding:0 var(--pad);
  display:grid;gap:12px 16px;grid-template-columns:1fr 1fr
}
.sfield{position:relative;display:block;min-width:0}
.stag{
  display:block;margin:0 0 6px 2px;
  font:700 9.5px/1.5 var(--mono);letter-spacing:.12em;text-transform:uppercase;
  color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}
/* On a phone the scope note wraps to a second line rather than being cut
   mid-word ("NOT COMPONEN"). */
@media (max-width:560px){ .stag{white-space:normal;overflow:visible;text-overflow:clip} }
.stag b{color:var(--cyan-ink);font-weight:700}
.sin{position:relative}
.sin .sic{
  position:absolute;left:12px;top:50%;width:17px;height:17px;
  transform:translateY(-50%);color:var(--faint);pointer-events:none;display:block
}
.sin .sic .ic{display:block;width:17px;height:17px}
/* ⚠️ This block was `.sfield input` only. The cabinet "add component" picker
   uses a bare `.sin` with no `.sfield` wrapper around it, so its input matched
   nothing and fell back to the browser default `<input type=search>` width —
   about 170px, with the placeholder running underneath the absolutely
   positioned magnifier. Exactly the same mistake as the account-page password
   fields (see the note further down). Matching on `.sin > input` fixes both
   and any future picker, because `.sin` is what actually owns the icon. */
.sfield input,
.sin > input{
  width:100%;padding:10px 12px 10px 38px;border-radius:10px;
  background-color:var(--surface);border:1px solid var(--line);color:var(--text);
  font:400 14px var(--sans);
  transition:border-color .18s ease,box-shadow .18s ease
}
.sfield input::placeholder,
.sin > input::placeholder{color:var(--faint)}
.sfield input:hover,
.sin > input:hover{border-color:rgba(103,232,249,.4)}
.sfield input:focus,
.sin > input:focus{outline:none;border-color:var(--cyan-deep);box-shadow:0 0 0 3px var(--cyan-dim)}
.sfield input::-webkit-search-cancel-button,
.sin > input::-webkit-search-cancel-button{cursor:pointer}

/* Results list for the component search. Each hit says what, how many, and
   in which closet — the closet chip carries that box's own tint. */
.sres{
  position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:35;
  background:var(--surface-2);border:1px solid var(--line-2);border-radius:12px;
  box-shadow:0 22px 50px rgba(0,0,0,.55);
  max-height:min(60vh,440px);overflow-y:auto
}
.sitem{
  appearance:none;-webkit-appearance:none;width:100%;text-align:left;
  display:flex;align-items:center;gap:12px;
  padding:10px 14px;border:0;border-bottom:1px solid var(--line);
  background:transparent;color:var(--text);font-family:var(--sans);cursor:pointer
}
.sitem:last-of-type{border-bottom:0}
.sitem:hover,.sitem:focus-visible{background:var(--cyan-dim);outline:none}
.sitem .sn{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.sitem .sn b{font:600 14px/1.25 var(--sans);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sitem .sn span{font:400 11.5px var(--mono);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sitem .sq{flex:none;font:600 13px var(--display);letter-spacing:-.02em;color:var(--text)}
.sitem .sq.zero{color:var(--alert)}
.sitem .sq.low{color:var(--warn)}
.sitem .sw{
  flex:none;max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font:600 10.5px var(--mono);letter-spacing:.04em;color:var(--tint,var(--cyan-ink));
  padding:3px 9px;border-radius:999px;
  background:rgba(255,255,255,.05);border:1px solid var(--line)
}
.sitem.k1{--tint:#67E8F9}.sitem.k2{--tint:#7FA0E8}.sitem.k3{--tint:#A98BE0}
.sitem.k4{--tint:#5FC4B0}.sitem.k5{--tint:#D98BC4}
.snone{padding:11px 14px;color:var(--faint);font-size:13px}

/* ---- sign-in / sign-up panels (2026-08-27) -------------------------------
   .loginbox was a <form>; it is now a <div> holding two forms, so the tab
   switch cannot lose what has been typed in the other panel.

   ⚠️ THE PANE IS BLOCK FLOW, NOT FLEX. Every child here already carries its
   own top margin — .lgf 22px, .lgbtn 26px, .lgfoot 26px — so a flex `gap`
   ADDS to those instead of replacing them, and the form drifts apart a few
   pixels at every row. Do not put display:flex + gap back on .lgpane. */
.lgtabs{display:flex;gap:6px;margin:26px 0 0;padding:4px;border-radius:12px;
  background:var(--surface-2);border:1px solid var(--line-2)}
.lgtab{flex:1;padding:9px 12px;border:0;border-radius:9px;background:transparent;
  color:var(--faint);font:600 13.5px var(--sans);cursor:pointer;transition:.15s}
.lgtab:hover{color:var(--text)}
.lgtab.on{background:var(--cyan-dim);color:var(--cyan-ink)}
/* display:none on the inactive pane also keeps its `required` fields from
   blocking submission of the visible one. */
.lgpane{display:none}
.lgpane.on{display:block}

.pwbar{display:block;height:4px;margin-top:7px;border-radius:3px;background:var(--line-2);overflow:hidden}
.pwbar i{display:block;height:100%;width:0;border-radius:3px;transition:width .18s,background .18s}
.pwbar.weak   i{width:33%;background:var(--alert)}
.pwbar.fair   i{width:66%;background:var(--warn)}
.pwbar.strong i{width:100%;background:var(--ok,#5FC4B0)}
.pwtip{display:block;margin-top:5px;color:var(--faint);font-size:11.5px;line-height:1.45}

/* The no-password-reset warning. Deliberately loud: it is the one thing on
   this page that cannot be undone later.
   ⚠️ Explicit margin-top — this used to ride on the pane's flex gap, which
   is gone. Same for .cf-turnstile below. */
.lgnote{display:flex;gap:8px;align-items:flex-start;margin:22px 0 0;padding:9px 11px;
  border-radius:9px;background:rgba(232,106,106,.09);border:1px solid rgba(232,106,106,.28);
  color:var(--text);font-size:11.5px;line-height:1.5}
.lgnote .ic{flex-shrink:0;width:15px;height:15px;margin-top:1px;color:var(--alert)}

/* ⚠️ The Turnstile widget is an iframe that Cloudflare sizes itself, and its
   fixed width overhangs the card on a narrow phone — the logo ends up cut off
   past the right edge. The !important is not decoration: the iframe carries
   an inline width attribute from Cloudflare's script, and nothing weaker
   overrides it.
   ★ Centred rather than left-aligned. data-size="flexible" is only honoured
   by newer API versions, so the widget's real width is not ours to predict —
   whatever it comes out as, centring keeps the overhang even on both sides
   instead of dumping all of it on the right. */
.cf-turnstile{min-height:65px;margin-top:22px;display:flex;justify-content:center}
.cf-turnstile iframe{max-width:100%!important;display:block;margin:0 auto}

/* ---- verify / reset pages (2026-08-27) ---------------------------------- */
/* The success twin of .lgerr — same shape, so a confirmation and a failure
   occupy identical space and the page does not jump between them. */
.lgok{
  display:flex;align-items:flex-start;gap:9px;margin:24px 0 -2px;
  padding:11px 13px;border-radius:10px;font-size:12.5px;line-height:1.45;
  background:rgba(95,196,176,.12);border:1px solid rgba(95,196,176,.34);color:#8FE3D2
}
.lgok .ic{width:15px;height:15px;flex:none;margin-top:1px}
.lgwho{margin:22px 0 0;color:var(--faint);font-size:12.5px}
.lgwho b{color:var(--text)}
.lgforgot{margin:14px 0 0;text-align:center}
.lglink{color:var(--cyan-ink);text-decoration:none;font-size:12.5px}
.lglink:hover{text-decoration:underline}
/* "optional" beside a field label — must read as quieter than the label
   itself, or it competes with it for attention. */
.lgopt{
  margin-left:6px;font:600 9.5px var(--mono);letter-spacing:.08em;
  font-style:normal;text-transform:uppercase;color:var(--muted);opacity:.75
}
/* .lgbtn is used on <a> as well as <button> on the verify page. */
a.lgbtn{text-decoration:none}

/* ---- account settings (2026-08-27) --------------------------------------- */
.acct{
  display:grid;gap:18px;
  /* ⚠️ GUTTERS. This was the only section on the site with neither a max-width
     nor horizontal padding, so the cards ran to the raw viewport edges while
     the header above them stopped at var(--pad) — the left and right margins
     did not line up with anything. Same box as `header` and `.sgroup`:
     max-width:var(--maxw) with padding:0 var(--pad), so the card edges sit
     directly under the header's logo and buttons.
     The bottom padding is here for the reason given on .addsec — a bottom
     MARGIN would collapse through .wrap and body and give no gap at all. */
  max-width:var(--maxw);margin:20px auto 0;padding:0 var(--pad) 60px;
  grid-template-columns:1fr 1fr;
  align-items:start
}
/* ★ THE FIX FOR THE DEAD SPACE. With the three cards as direct grid items,
   grid put the full-width red card on its own row — and a row cannot start
   until the tallest item in the row above has finished. The tall password
   form therefore held the red card down, leaving a void the height of that
   form sitting under the short email card. Two independent column stacks
   share no row, so each card starts exactly where the one above it ended. */
.acol{display:flex;flex-direction:column;gap:18px;min-width:0}

/* One column on a narrow screen. `display:contents` dissolves the wrappers
   so all three cards become direct flex children again, which is what lets
   the red card be re-ordered last — otherwise it would sit in the middle,
   between the email card and the password form. */
@media (max-width:720px){
  .acct{display:flex;flex-direction:column}
  .acol{display:contents}
  .acard.danger{order:3}
}
.acard{
  background:var(--surface);border:1px solid var(--line-2);border-radius:16px;padding:20px 22px
}
.acard h2{
  display:flex;align-items:center;gap:8px;margin:0 0 6px;
  font:700 15px var(--display);color:var(--text)
}
.acard h2 .ic{width:16px;height:16px;color:var(--cyan-ink)}
.ahint{margin:0 0 14px;color:var(--faint);font-size:12px;line-height:1.55}
.warnhint{display:flex;gap:7px;align-items:flex-start;color:var(--warn)}
.warnhint .ic{flex:none;width:14px;height:14px;margin-top:1px}

/* Current state, colour-coded. "Unconfirmed" is amber not green — it is worth
   the same as no address at all when a password actually needs resetting. */
.astate{
  display:flex;align-items:flex-start;gap:10px;
  margin:0 0 16px;padding:10px 12px;border-radius:9px;font-size:12.5px;line-height:1.5
}
.astate > span{flex:1;min-width:0}
.astate.good{background:rgba(95,196,176,.12);border:1px solid rgba(95,196,176,.32);color:#8FE3D2}
.astate.wait{background:rgba(224,168,90,.12);border:1px solid rgba(224,168,90,.32);color:var(--warn)}
.astate.none{background:rgba(232,106,106,.10);border:1px solid rgba(232,106,106,.30);color:#FCA5A5}

/* ★ Dismiss appears on the CONFIRMED state only. The amber and red states are
   asking for something to be done and must not be dismissible — hiding them
   would hide the fact that password reset is still unavailable. Green has
   already served its purpose the moment it is read. */
.axb{
  appearance:none;background:none;border:0;cursor:pointer;padding:3px;
  border-radius:6px;color:inherit;opacity:.55;flex:none;
  display:inline-flex;align-items:center;
  transition:opacity .13s ease,background-color .13s ease
}
.axb .ic{width:14px;height:14px}
.axb:hover{opacity:1;background-color:rgba(255,255,255,.1)}
.axb:focus-visible{opacity:1;outline:2px solid currentColor;outline-offset:1px}

.arow{display:flex;gap:9px;align-items:stretch;margin-bottom:11px}
.arow input{flex:1;min-width:0}
.acard .lgf{margin-top:16px}
.acard .lgbtn{margin-top:20px}

/* ⚠️ The field styling lived ONLY under `.loginbox input`, and the account
   page is not a .loginbox — so its password fields fell back to the browser
   default width (~150px) and sat as three stubby boxes in a wide card.
   Same rules, applied here too. */
.acard input:not([type=hidden]){
  width:100%;padding:12px 14px;border-radius:10px;
  background-color:var(--bg);border:1px solid var(--line);
  color:var(--text);font:400 14.5px var(--sans);
  transition:border-color .16s ease,box-shadow .16s ease,background-image .18s ease
}
.acard input:hover{
  border-color:rgba(103,232,249,.34);
  background-image:radial-gradient(200px circle at var(--mx,50%) var(--my,50%),
    rgba(103,232,249,.14),transparent 68%)
}
.acard input:focus{
  outline:none;border-color:var(--cyan-deep);
  box-shadow:0 0 0 3px rgba(103,232,249,.14)
}
/* Chrome's pale autofill fill is unreadable on a dark form and only an inset
   shadow covers it — same treatment as the login card. */
.acard input:-webkit-autofill,
.acard input:-webkit-autofill:hover,
.acard input:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--text);
  -webkit-box-shadow:0 0 0 40px var(--bg) inset;
  caret-color:var(--text)
}

/* The dot on the Account button. Quiet on purpose — nothing here is broken,
   there is simply something worth doing. */
.btn.nag{border-color:rgba(224,168,90,.42)}
.btn.nag .dot{
  width:6px;height:6px;margin-left:6px;border-radius:50%;
  background:var(--warn);display:inline-block;flex:none
}

/* ---- the "edited" mark on a shared catalogue part ------------------------
   ★ Permanent by design — it records that the shared description changed
   after creation, which is a fact about history, not a pending state. The
   part stays fully usable, so this must read as information and never as a
   warning that something is broken. */
.edtag{display:inline-flex;align-items:center;gap:3px;margin-left:7px;
  padding:1px 6px;border-radius:5px;vertical-align:middle;
  background:rgba(224,168,90,.14);border:1px solid rgba(224,168,90,.32);
  color:var(--warn);font:600 10px var(--mono);letter-spacing:.03em;
  text-transform:uppercase;cursor:help}
.edtag .ic{width:11px;height:11px}

/* Hint under the picker in a cabinet: the picker only finds what already
   exists, so the way to add something new has to be visible right there. */
.pnote{margin:8px 18px 0;color:var(--faint);font-size:11.5px}
.pnote b{color:var(--cyan-ink)}

/* ★ The private shelf chip (migrate-010). Deliberately NOT styled like the
   part number beside it: the part number is shared with every user, the
   shelf is only ever yours. Same cyan family as the "in this cabinet" chip
   on requirement rows, because both answer "where is it". */
.cmeta .shelf{
  display:inline-flex;align-items:center;padding:1px 7px;border-radius:5px;
  background:var(--cyan-dim);color:var(--cyan-ink);
  font:600 10.5px var(--mono);letter-spacing:.04em;cursor:help
}
/* The edited marker inside a picker result — quieter than the row badge,
   which has room to explain itself on hover. */
.sitem .sedit{
  flex:none;padding:1px 6px;border-radius:5px;
  background:rgba(224,168,90,.14);border:1px solid rgba(224,168,90,.3);
  color:var(--warn);font:600 9.5px var(--mono);
  letter-spacing:.05em;text-transform:uppercase
}

/* ---- the lecture picker + requirement rows (2026-08-27, migrate-005) ----
   A lecture box borrows .sitem / .snone from the dashboard search above, so
   the dropdown reads identically in both places. Only what differs is set
   here: it is positioned inside a popup rather than the page.

   ⚠️ .pickbar must establish a stacking context ABOVE .rows or the dropdown
   renders behind the first requirement row. The popup is already scrolled
   content, so a plain z-index without position does nothing. */
.pickbar{position:relative;z-index:12;padding:12px 18px 0}
.pres{
  position:absolute;left:18px;right:18px;top:calc(100% + 6px);z-index:14;
  background:var(--surface-2);border:1px solid var(--line-2);border-radius:12px;
  box-shadow:0 22px 50px rgba(0,0,0,.55);
  max-height:min(48vh,360px);overflow-y:auto
}
.pres .sitem{--tint:var(--cyan-ink)}
/* Already required by this box: shown, not hidden. A result that vanishes
   reads as "the lab does not have one", which is the opposite of the truth. */
.pres .sitem.isin{opacity:.45;cursor:default}
.pres .sitem.isin:hover{background:transparent}
.pickform{display:none}

/* The requirement row. Same shell as .crow so the two popups feel like one
   app; the extra parts are the home-closet chip, the read-only stock block
   and the over-stock badge. */
.rrow .rin{
  display:inline-flex;align-items:center;padding:1px 7px;border-radius:5px;
  background:var(--cyan-dim);color:var(--cyan-ink);
  font:600 10.5px var(--mono);letter-spacing:.04em
}
.stockv{
  flex-shrink:0;min-width:58px;text-align:right;
  display:flex;flex-direction:column;align-items:flex-end;gap:1px;
  font:600 16px var(--display);color:var(--text)
}
.stockv em{
  font:700 9px var(--mono);letter-spacing:.12em;text-transform:uppercase;
  font-style:normal;color:var(--faint)
}
.stockv.zero{color:var(--alert)}
.stockv.low{color:var(--warn)}

/* ★ The over-stock badge is permanent, not a flash. The save already said it
   once; this is what makes the shortfall visible every time the box is
   opened. Red border on the whole row so it is findable while scrolling. */
.rrow.isover{border-color:rgba(232,106,106,.42);background-color:rgba(232,106,106,.05)}
.over{
  display:inline-flex;align-items:center;margin-top:4px;padding:2px 8px;
  border-radius:5px;background:var(--alert-dim,rgba(232,106,106,.14));
  color:var(--alert);border:1px solid rgba(232,106,106,.3);
  font:600 11px var(--sans)
}
.qbox.qreq .qnum{width:56px}

@media (max-width:620px){
  .stockv{min-width:44px}
  .pres{left:14px;right:14px}
}

/* Class/box filter: hidden things stay in the DOM so the drag code's
   order() still sees the whole section — drag is simply disabled while a
   filter is active (body.qfilter). */
.sgroup.shide,.ccard.shide{display:none}
.sempty{max-width:var(--maxw);margin:18px auto 0;padding:0 var(--pad);color:var(--faint);font-size:13px}

/* ---------- drag to reorder boxes (2026-08-27) ---------------------------
   pan-y: a sideways finger drag reaches the script, an up/down one scrolls
   the page as before. The lifted card drops its transform transition or it
   would trail the pointer by a quarter second. */
.ccard{touch-action:pan-y}
/* ---------- box logo (2026-08-27) ----------------------------------------
   The logo takes the tile's place: same 42px rounded square, the tint
   stays underneath so a transparent PNG still sits on the box's colour. */
.si.haslogo{overflow:hidden;padding:0}
.si img{width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit}
.sheethead .hlogo{
  width:38px;height:38px;flex:none;object-fit:cover;border-radius:10px;
  border:1px solid var(--line);background:var(--surface-2);cursor:zoom-in;
  transition:border-color .14s ease
}
.sheethead .hlogo:hover{border-color:rgba(103,232,249,.5)}
/* .t grows so the logo + title hug the left and the actions keep the right;
   without this, space-between would float the title into the middle. */
.sheethead .t{flex:1 1 auto;min-width:0}
.ccard.dragging{
  position:relative;z-index:40;cursor:grabbing;transition:none;
  border-color:rgba(103,232,249,.55);
  box-shadow:0 18px 46px rgba(0,0,0,.55);
  background-image:none
}
body.bdrag{user-select:none;-webkit-user-select:none;cursor:grabbing}
body.bdrag .ccard:hover{transform:none;box-shadow:none;background-image:none}

@media (max-width:820px){
  .searchbar{grid-template-columns:1fr}
}

/* ---------- account: danger zone (2026-08-28) ---------------------------
   ★ Bottom of the left column, under the email card. It was full-width across
   the grid, which read better in isolation but forced an empty row the height
   of the password form — see the .acol note above. Sitting under the short
   card keeps it below both safe options on the way down the left, and the
   media query puts it last again once the layout is a single column.
   --alert is verdict colour (see the palette note at the top of this file) —
   this is the one place on the page entitled to it. */
.acard.danger{
  border-color:rgba(242,109,120,.30);
  background-image:radial-gradient(520px circle at 0% 0%,
    rgba(242,109,120,.07),transparent 70%)
}
.acard.danger h2 .ic{color:var(--alert)}
/* The card holds three short things and then stopped with a band of empty
   space under the button. Tightened top and bottom so it reads as one compact
   block rather than a half-empty panel. */
.acard.danger{padding-bottom:18px}
.acard.danger .ahint{margin-bottom:13px}
.acard.danger .btn{margin:0}

/* The dialog's warning line. Spans both columns of .fbody, and sits above the
   two fields so it is read before anything can be typed. */
.dzmsg{
  grid-column:1/-1;margin:2px 0 4px;padding:11px 13px;border-radius:10px;
  background:var(--alert-dim);border:1px solid rgba(242,109,120,.28);
  color:#FCA5A5;font:400 12.5px/1.6 var(--sans)
}
.dzmsg b{color:#FECACA;font-weight:700}

/* ---------- account: delete dialog, code step (2026-08-28) --------------
   Replaced the "type your email address" box. Typing an address already on
   screen proved only that the person could read it; a code that has to be
   fetched from the mailbox is a real second factor and less work. */
.dzask{
  grid-column:1/-1;margin:0;color:var(--faint);font:400 12.5px/1.6 var(--sans)
}
.dzask b{color:var(--text)}
.dzsent{
  grid-column:1/-1;display:flex;gap:8px;align-items:flex-start;margin:0 0 2px;
  padding:9px 12px;border-radius:9px;font:400 12.5px/1.5 var(--sans);
  background:rgba(95,196,176,.12);border:1px solid rgba(95,196,176,.32);color:#8FE3D2
}
.dzsent .ic{flex:none;width:14px;height:14px;margin-top:2px}
/* The code is six characters and nothing else — wide tracking and a monospace
   face so a mistyped digit is visible before the button is pressed. */
.dzcode{
  font:700 20px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace !important;
  letter-spacing:9px;text-align:center
}
.dzcode::placeholder{letter-spacing:9px;opacity:.35}

/* ---------- search dropdown thumbnails (2026-08-28) ---------------------
   Smaller than the 52px .thumb used inside a cabinet: a dropdown row is a
   glance on the way to somewhere else, not the record itself, and 52px would
   make twenty results taller than the viewport. 38px keeps the row height
   driven by the two lines of text that were already there. */
.sitem .sthumb{
  width:38px;height:38px;flex:none;border-radius:7px;object-fit:cover;
  background:var(--surface-2);border:1px solid var(--line)
}
.sitem .sthumb.ph{
  display:flex;align-items:center;justify-content:center;
  font:700 7.5px var(--mono);letter-spacing:.08em;color:var(--faint)
}
/* Greyed with the rest of an "already here" row so the picture does not stay
   bright on a result that cannot be picked. */
.sitem.isin .sthumb{opacity:.6}
