:root {
  --bg: #0f1115; --panel: #171a21; --line: #262b36; --text: #e6e9ef;
  --muted: #8b93a3; --accent: #5b9dff; --up: #37c07a; --down: #ff6b6b;
  --warn: #ffb454; --urgent: #ff5c5c;
  /* #107 — links are their own colour, a step brighter than --accent so they
     stay legible on the panels as well as on the page. */
  --link: #8ab8ff; --link-hover: #c2d9ff;
}
* { box-sizing: border-box; }
/* the hidden attribute must always win — otherwise display rules like .btn
   (display:inline-block) keep a hidden element visible (issue #43). */
[hidden] { display: none !important; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font: 15px/1.5 system-ui, "Segoe UI", Roboto, sans-serif;
}
/* #107 — nothing here styled a bare <a>, so every link without a class of its
   own fell back to the browser's built-in navy (#00e on a #0f1115 page: barely
   a link, barely visible) and to purple once followed. Links now carry --link,
   and a visited one is not dimmed — a dark visited colour is the same trap.
   The state selectors sit inside :where() so they weigh nothing: a link that
   already takes a colour from its class — .brand, .pf-card, .expiry-link,
   `nav a` — keeps that colour while hovered instead of being outranked here. */
a, a:where(:visited) { color: var(--link); }
a:where(:hover, :focus-visible) { color: var(--link-hover); }
.topbar {
  display: flex; align-items: center; gap: 1.5rem;
  padding: .8rem 1.2rem; background: var(--panel); border-bottom: 1px solid var(--line);
}
.brand { font-weight: 700; letter-spacing: .5px; color: var(--text); text-decoration: none; display: inline-flex; align-items: center; gap: .4rem; }
.brand:hover { color: var(--accent); }
.brand-logo { width: 20px; height: 20px; display: block; }
nav { display: flex; align-items: center; gap: 1rem; }
.crumbs .crumb-sep { color: var(--muted); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: .9rem; }
/* #71 — the guide "?"; #74 moved it out of the top bar to sit beside the
   "Your portfolios" heading on the main page */
.guide-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; border-radius: 50%;
  border: 1px solid var(--line); color: var(--muted);
  text-decoration: none; font-weight: 700; font-size: .85rem; line-height: 1;
}
.guide-link:hover { color: var(--accent); border-color: var(--accent); }
/* #70 — language switch: both flags always visible, the current one ringed.
   The flags are circular so the ring reads as a ring rather than as a border. */
.lang-switch { display: inline-flex; align-items: center; gap: .35rem; }
.lang-flag {
  display: inline-flex; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  overflow: hidden; box-shadow: 0 0 0 1px var(--line); opacity: .55;
  transition: opacity .12s ease;
}
.lang-flag svg { width: 100%; height: 100%; display: block; }
a.lang-flag:hover { opacity: 1; box-shadow: 0 0 0 1px var(--accent); }
/* the ring: a gap in the page background, then the accent, so it reads at 20px */
.lang-flag.current {
  opacity: 1; cursor: default;
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--accent);
  margin: 0 3px;                      /* room for the ring inside the flex row */
}
.profile-link { color: var(--text); text-decoration: none; display: inline-flex; align-items: center; gap: .35rem; }
.profile-link:hover { color: var(--accent); }
.profile-link .pfn { font-weight: 600; }
.bell { cursor: pointer; background: transparent; border: 1px solid var(--line); border-radius: 999px; padding: .2rem .7rem; color: var(--text); font: inherit; }
.bell.on { color: var(--up); border-color: var(--up); }
.linkbtn.disabled { opacity: .4; cursor: not-allowed; }
nav a, nav .active { color: var(--muted); text-decoration: none; }
nav a.active, nav .active { color: var(--text); }
nav a:hover { color: var(--accent); }
main { max-width: 920px; margin: 1.5rem auto; padding: 0 1.2rem; }
/* #89 — the localhost admin is a table, not an article: it gets the whole
   window, so a wide screen shows the session columns instead of wrapping them.
   Scoped to <body class="admin">, so the public pages keep their reading width. */
body.admin main { max-width: none; }
h1 { font-size: 1.4rem; } h2 { font-size: 1.05rem; margin-top: 1.6rem; }
.muted { color: var(--muted); }
.up { color: var(--up); }
.down { color: var(--down); }
.foot { text-align: center; color: var(--muted); font-size: .8rem; padding: 2rem 0; }
/* #77 — the version reads as an identifier, not as prose */
.foot .ver { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }

.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 1.1rem; margin: 1rem 0; }
.card.narrow { max-width: 420px; margin: 3rem auto; }

label { display: block; margin: .5rem 0; color: var(--muted); font-size: .85rem; }
input {
  display: block; width: 100%; margin-top: .25rem; padding: .5rem .6rem;
  background: var(--bg); border: 1px solid var(--line); border-radius: 7px; color: var(--text);
}
.add-pos { margin: .2rem 0 .7rem; }
.add-pos-form { margin-top: .3rem; }
.addline { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.addline input { margin: 0; }
.addline .in-asset { width: 6rem; text-transform: uppercase; }
.addline .in-num { width: 7rem; }
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem 1rem; }
.grid .wide { grid-column: 1 / -1; }
form.inline { display: inline; }

/* compact inline sale editor: field name on the left, input on the right,
   minimal vertical padding (issue #50 — the stacked form was too tall) */
.sale-edit-form { margin: .1rem 0; }
.edit-grid { border-collapse: collapse; width: 100%; max-width: 26rem; }
.edit-grid th { text-align: left; font-weight: 500; color: var(--muted);
  font-size: .85rem; white-space: nowrap; padding: .12rem .8rem .12rem 0; vertical-align: middle; }
.edit-grid td { padding: .12rem 0; }
.edit-grid input { margin: 0; padding: .3rem .5rem; }
.edit-grid .edit-actions { padding-top: .5rem; }

.btn {
  display: inline-block; margin-top: .6rem; padding: .5rem .9rem; border: 0; cursor: pointer;
  background: var(--accent); color: #0b1020; border-radius: 7px; font-weight: 600; text-decoration: none;
}
.btn.ghost { background: transparent; color: var(--text); border: 1px solid var(--line); }
.btn.sm { padding: .3rem .6rem; font-size: .85rem; margin: 0; }
.btn[disabled], input[disabled] { opacity: .45; cursor: not-allowed; }
.linkbtn { background: none; border: 0; color: var(--accent); cursor: pointer; padding: 0 .3rem; font: inherit; }
.linkbtn.danger, .danger { color: var(--down); }
/* #75 — the delete-account block. Set apart from the rest of the profile because
   it is the only control on the site that ends everything the user has stored. */
.danger-zone { border-color: rgba(233,86,86,.4); }
.danger-zone h2 { color: var(--down); }
.delete-account label { display: block; margin-top: .6rem; font-size: .85rem; color: var(--muted); }
.delete-account input { max-width: 22rem; }

.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: .8rem; margin: 1rem 0; }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: .8rem; }
.tile span { display: block; color: var(--muted); font-size: .78rem; }
.tile strong { font-size: 1.3rem; }
.tile.up strong { color: var(--up); } .tile.down strong { color: var(--down); }

table { width: 100%; border-collapse: collapse; margin-top: .6rem; }
th, td { text-align: left; padding: .45rem .5rem; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 600; font-size: .8rem; }
.r { text-align: right; } .nowrap { white-space: nowrap; }
td.up { color: var(--up); } td.down { color: var(--down); }

.actions { list-style: none; padding: 0; }
.actions li { padding: .5rem .7rem; border-left: 3px solid var(--line); background: var(--panel); margin: .35rem 0; border-radius: 0 7px 7px 0; }
.actions .sev-warn { border-color: var(--warn); }
.actions .sev-urgent { border-color: var(--urgent); }
.tag { display: inline-block; background: var(--bg); border: 1px solid var(--line); border-radius: 5px; padding: 0 .4rem; margin-right: .4rem; font-size: .8rem; }
.badge { display: inline-block; font-size: .68rem; font-weight: 700; letter-spacing: .5px; padding: .05rem .35rem; border-radius: 4px; vertical-align: middle; }
.badge.new { background: var(--warn); color: #1a1204; }

/* admin: users + sessions */
.days { width: 4.5rem; display: inline-block; margin: 0 .2rem 0 0; padding: .25rem .35rem; text-align: right; }
.cur-prefix { color: var(--muted); margin-right: .15rem; }
/* compact portfolio summary (two-row mini table) */
.pf-summary { border-collapse: collapse; margin: .2rem 0 .5rem; }
.pf-summary th { text-align: left; color: var(--muted); font-weight: 500; padding: .08rem 1.4rem .08rem 0; white-space: nowrap; }
.pf-summary td { padding: .08rem 2rem .08rem 0; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pf-summary td.up { color: var(--up); } .pf-summary td.down { color: var(--down); }
@media (max-width: 480px) { .pf-summary th { padding-right: .6rem; } .pf-summary td { padding-right: 1rem; } }

/* contextual help icon + popover (hidden until the ? is tapped) */
.help-wrap { position: relative; display: inline-block; }
/* superscript look via a relative offset (visual only) so it can't change the
   line-box height or push the label / amount up or down */
.help { display: inline-flex; align-items: center; justify-content: center; width: 1.15em; height: 1.15em; font-size: .7em; vertical-align: baseline; position: relative; top: -.35em; border-radius: 50%; border: 1px solid var(--line); background: transparent; color: var(--muted); cursor: pointer; padding: 0; line-height: 1; }
.help:hover { color: var(--accent); border-color: var(--accent); }
.help-pop { position: absolute; left: 0; top: 1.6em; z-index: 30; min-width: 180px; max-width: 260px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: .5rem .6rem; color: var(--text); font-size: .82rem; font-weight: 400; line-height: 1.4; box-shadow: 0 6px 18px rgba(0,0,0,.45); white-space: normal; }

.page-status { margin: .1rem 0 .4rem; font-size: .85rem; display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.bell-btn { background: transparent; border: 0; cursor: pointer; font-size: 1.1rem; line-height: 1; padding: .1rem .3rem; border-radius: 6px; }
.bell-btn.off { filter: grayscale(1); opacity: .7; }
.bell-btn:hover { background: var(--panel); }
.notify-warn { margin: .1rem 0 .5rem; font-size: .82rem; }
/* Update Now on the left, timestamp pinned right at a constant width so it
   doesn't jitter as the "ago" count changes. */
.times { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin: .3rem 0 .6rem; }
.times .times-right { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.times .ago { display: inline-block; min-width: 5.5rem; text-align: left; }
.times .sep { color: var(--line); }

/* Free Budget row — identical height/alignment across view <-> edit so nothing
   below it moves when the editor opens */
.budget-row { align-items: center; min-height: 2.4rem; }
.budget-label { margin: 0; }
.budget-edit { display: flex; align-items: center; min-height: 2rem; }
#budget-view { display: inline-flex; align-items: center; gap: .3rem; min-height: 2rem; }
.budget-form { display: flex; align-items: center; gap: .3rem; min-height: 2rem; }
.budget-in { width: 7rem; height: 1.8rem; margin: 0; padding: .1rem .35rem; text-align: right; }
.iconbtn { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; line-height: 1; padding: .1rem .25rem; }
.iconbtn.save { font-size: 1.05rem; }
.iconbtn:hover { filter: brightness(1.2); }
#update-btn[disabled] { opacity: .6; cursor: default; }
.user { padding: .9rem 1rem; }
.user-head { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; margin-bottom: .3rem; }
/* #81 — the account rows are collapsed until you open one, so the panel starts
   as a short list. `display:flex` on a <summary> drops the native disclosure
   triangle in WebKit/Blink, so the caret is drawn here instead. */
summary.user-head { cursor: pointer; list-style: none; }
summary.user-head::-webkit-details-marker { display: none; }
summary.user-head::before { content: '▸'; color: var(--muted); }
details[open] > summary.user-head::before { content: '▾'; }
.user-body { margin-top: .5rem; }
.user-head .account { font-size: 1.05rem; }
.pill { display: inline-block; font-size: .72rem; font-weight: 600; padding: .08rem .5rem; border-radius: 999px; border: 1px solid var(--line); }
.pill.ok { color: var(--up); border-color: rgba(55,192,122,.4); }
.pill.off { color: var(--muted); }
/* #75 — an account counting down to permanent deletion. Loud on purpose: the
   operator has a limited number of days to change their mind about it. */
.pill.danger { color: var(--down); border-color: var(--down); }
.badge.dying { background: var(--down); color: #2a0808; }
.card.user.dying { border-color: rgba(233,86,86,.45); }
/* #94 — the unattended worker's own state, read off its execution log. Sits at
   the top of the panel because a stopped worker is the one thing here that no
   other row on the page would ever reveal. */
.worker-grid { display: flex; flex-wrap: wrap; gap: .8rem; }
.worker-card {
  flex: 1 1 320px; background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid var(--line); border-radius: 10px; padding: .8rem 1rem;
}
.worker-card.ok, .worker-card.running { border-left-color: var(--up); }
.worker-card.late { border-left-color: var(--warn); }
.worker-card.failing, .worker-card.killed { border-left-color: var(--down); }
.worker-top { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; }
.worker-name { font-weight: 700; }
.worker-card p { margin: .35rem 0; }
.worker-what { font-size: .8rem; }
.worker-when { font-size: .9rem; }
.worker-summary { color: var(--text); }
.worker-trouble { color: var(--down); font-weight: 600; font-size: .85rem; }
/* One square per run the log still covers, oldest first: a row of green with a
   red square in it says "it broke once and recovered" at a glance. */
.worker-runs { display: flex; gap: 3px; margin-top: .5rem; }
.worker-runs .run { width: 12px; height: 12px; border-radius: 3px; background: var(--muted); }
.worker-runs .run.ok { background: var(--up); }
.worker-runs .run.bad { background: var(--down); }
.worker-runs .run.killed { background: var(--warn); }
/* #87 — the panel heading, with the zone every timestamp on it is read in
   pushed to the far side. */
.admin-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.admin-head h1 { margin: 0; }
.tz-pick { display: inline-flex; align-items: center; gap: .4rem; }
.tz-pick select { background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
                  color: var(--text); font: inherit; font-size: .85rem; padding: .2rem .4rem; }
.inline-label { display: inline; margin: 0; color: var(--muted); font-size: .85rem; }
/* #88 — the operator's notes: quiet until there is one to read, and never wide
   enough to push the columns around. */
.note-cell { max-width: 18rem; }
.note-view { display: inline-flex; align-items: baseline; gap: .3rem; }
.note-label { color: var(--muted); font-size: .85rem; }
.note-text { font-size: .85rem; white-space: normal; overflow-wrap: break-word; }
.note-in { display: inline-block; width: 14rem; margin: 0; padding: .15rem .35rem; font-size: .85rem; }
.note-edit { display: inline-flex; align-items: center; gap: .3rem; }
/* #84 — the default new approvals inherit, changed where they are granted. */
.default-days { display: inline-flex; align-items: center; gap: .4rem; margin: .4rem 0; }
.default-days .days { margin: 0; }
/* #82 — the session identifier reads as an id, not as prose, and never competes
   with the device name next to it. */
.sid { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: .8rem; color: var(--muted); }
.reason { margin-left: .3rem; }
.toggle { cursor: pointer; font-size: .72rem; font-weight: 700; letter-spacing: .5px; padding: .15rem .55rem; border-radius: 999px; border: 1px solid var(--line); background: transparent; }
.toggle.on { color: var(--up); border-color: var(--up); }
.toggle.off { color: var(--muted); }

/* portfolios hub */
/* #69 — the "Your portfolios" line, with the session PIN lock pushed to its
   rightmost edge. */
.page-head { display: flex; align-items: center; gap: .6rem; }
.page-head h1 { margin: 0; }
/* Inline SVG, not the 🔓/🔒 emoji: no PIN must read as a *snapped* padlock, and
   emoji only offers an intact one standing open (see _icons.html). */
.pin-lock { margin-left: auto; cursor: pointer; line-height: 1; display: inline-flex;
            padding: .25rem .35rem; border-radius: 7px; border: 1px solid transparent;
            user-select: none; -webkit-user-select: none; color: var(--muted); }
.pin-lock svg { width: 1.35rem; height: 1.35rem; display: block; }
.pin-lock:hover, .pin-lock:focus { border-color: var(--accent); color: var(--text); outline: none; }
.pin-lock.on { color: var(--accent); }            /* PIN set: the lock is live, show it lit */
/* the padlock beside the "Locked" heading, sized to the text it sits in */
.lock-mark { display: inline-flex; vertical-align: -.12em; }
.lock-mark svg { width: 1em; height: 1em; }

/* #69 — the PIN column in the localhost session admin: state first, then the
   two operator actions (clear a lockout / switch the PIN off). */
.pin-cell .sm { font-size: .78rem; }
.pin-cell .pin-acts { display: inline-flex; gap: .3rem; margin-left: .4rem; }

/* #69 — in-DOM dialog (never a browser-native prompt). Closes on X, on a click
   on this backdrop, or on Escape — none of which change the setting. */
.modal { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center;
         justify-content: center; padding: 1rem; background: rgba(0,0,0,.6); }
.modal-card { position: relative; background: var(--panel); border: 1px solid var(--line);
              border-radius: 10px; padding: 1.1rem 1.2rem; width: 100%; max-width: 360px;
              box-shadow: 0 10px 30px rgba(0,0,0,.5); }
.modal-card h2 { margin: 0 2rem .4rem 0; font-size: 1.05rem; }
.modal-x { position: absolute; top: .4rem; right: .5rem; background: none; border: 0;
           color: var(--muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: .1rem .3rem; }
.modal-x:hover { color: var(--text); }
.modal-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.lock-logout { margin-top: 1.2rem; border-top: 1px solid var(--line); padding-top: .6rem; }

/* Tight portfolio list — no gaps between cards; collapse the shared borders so
   adjacent cards read as one continuous list. */
.pf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0; margin: 1rem 0; }
.pf-card { display: block; text-decoration: none; color: inherit; border-radius: 0; margin: -1px 0 0 -1px; }
.pf-card:first-child { margin-top: 0; }
.pf-card:hover { border-color: var(--accent); position: relative; z-index: 1; }
.pf-top { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
.pf-top strong { font-size: 1.05rem; flex: 1; }
/* #59: drag handle. touch-action:none so a touch-drag reorders instead of scrolling. */
.pf-handle { cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none;
             color: var(--muted); line-height: 1; padding: .15rem .2rem; border-radius: 4px; }
.pf-handle:hover, .pf-handle:focus { color: var(--text); outline: 1px solid var(--accent); }
.pf-card.pf-dragging { cursor: grabbing; opacity: .85; position: relative; z-index: 5;
                       border-color: var(--accent); box-shadow: 0 4px 14px rgba(0,0,0,.35); }
.pf-card.pf-dragging .pf-handle { cursor: grabbing; }
.pf-stats { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; margin-top: .5rem; font-size: .9rem; color: var(--muted); }
.pf-stats .amt { font-variant-numeric: tabular-nums; white-space: nowrap; }
.pf-reveal { margin-top: .5rem; }
.pf-reveal .reveal-btn { color: var(--muted); font-weight: 500; }
.pf-reveal .reveal-btn:hover { color: var(--text); border-color: var(--accent); }
.pf-alert { margin-top: .5rem; color: var(--urgent); font-weight: 600; font-size: .85rem; }
.narrow-ish { max-width: 460px; }

/* portfolio detail */
.perf-list { list-style: none; padding: 0; margin: .5rem 0; max-width: 280px; }
.perf-list li { display: flex; justify-content: space-between; padding: .35rem .1rem; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.perf-list .perf-name { font-weight: 600; }

.pf-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
/* #76 — the restore menu sits with the delete link, both pushed to the right */
.pf-acts { display: flex; align-items: baseline; gap: .9rem; margin-left: auto; }
.modal-card.wide { max-width: 44rem; max-height: 82vh; overflow-y: auto; }
.restore-list td { vertical-align: middle; }
.restore-when { margin: .2rem 0 .1rem; font-size: 1rem; }
.restore-diff { margin: .6rem 0 1rem; }
.restore-diff td, .restore-diff th { padding: .15rem .9rem .15rem 0; }
.restore-diff .differs { color: var(--warn); font-weight: 600; }
.restore-go { margin-top: .8rem; border-top: 1px solid var(--line); padding-top: .6rem; }

/* ---- #100 clickable tickers + the stock dialog ----------------------------
   The ticker keeps the weight and colour it had as plain text — it sits in a
   column of numbers, and turning it accent-blue would make every row shout.
   The underline on hover/focus is what says it can be clicked. */
.tickerbtn { background: none; border: 0; padding: 0; font: inherit; color: inherit;
             cursor: pointer; text-align: left; text-decoration: underline;
             text-decoration-style: dotted; text-underline-offset: .18em;
             text-decoration-color: var(--muted); overflow-wrap: break-word; }
.tickerbtn:hover, .tickerbtn:focus-visible { color: var(--accent); text-decoration-style: solid;
             text-decoration-color: currentColor; }
/* ---- #101 the smile: the owner's own mark, printed after the ticker --------
   `display:inline-block` is load-bearing — text-decoration propagates from the
   ticker button to its descendants, and an inline-block is where that stops, so
   the mark doesn't inherit the dotted underline that says "click me".
   It has a price: an inline-block is an atomic inline, so the browser may break
   the line between the ticker and the mark. `smile_sup` emits a WORD JOINER
   ahead of the <sup> to forbid exactly that — without it the mark drops to the
   next line in any column narrow enough to want the break.
   `vertical-align` is stated rather than left to the UA's default for <sup>,
   because it and the joiner are the two halves of "this reads as superscript"
   and they belong in one place. */
.smile { display: inline-block; vertical-align: super; text-decoration: none;
         margin-left: .1em; font-size: .7em; line-height: 1; }
/* #106 "Red and bold, if the symbol is just text." An emoji arrives with its
   own colour and its own weight; a letter arrives with neither, and at .7em it
   would be a grey speck beside the ticker. The class is put on by `smile_sup`
   (and by smilePaint after a save), because whether a mark is written or drawn
   is a question about its characters and CSS cannot ask it. */
.smile-text { color: var(--down); font-weight: 700; }
.stock-smile { margin: -.4rem 0 .9rem; }
.stock-smile-label { color: var(--muted); margin-right: .5rem; }
.stock-smile-field { width: 4.5rem; text-align: center; font-size: 1.1rem; }
/* #106 "Make the smile of the no smile gray out, not look like actual smile."
   The prompt in an empty field is the outline face in its text presentation
   (see _stock_info.html), and `opacity: 1` hands the greying to the muted
   colour instead of the browser's own washed-out default — so it reads as the
   hint it is, not as a mark this company already wears. */
.stock-smile-field::placeholder { color: var(--muted); opacity: 1; }
.stock-smile-state { margin-left: .5rem; }
.stock-smile-hint { margin: .35rem 0 0; }
.stock-name { margin: 0 0 .1rem; font-size: 1.05rem; overflow-wrap: break-word; }
.stock-sub { margin: 0 0 .8rem; font-size: .85rem; overflow-wrap: break-word; }
.stock-sub .stock-ticker { color: var(--text); font-variant-numeric: tabular-nums; }
.stock-facts { border-collapse: collapse; margin: 0 0 .9rem; width: 100%; }
.stock-facts th { text-align: left; color: var(--muted); font-weight: 500;
                  padding: .16rem 1.2rem .16rem 0; white-space: nowrap; vertical-align: top; }
.stock-facts td { padding: .16rem 0; font-variant-numeric: tabular-nums;
                  overflow-wrap: break-word; }
.stock-facts .stock-facts-head { color: var(--text); font-weight: 700; padding-top: .5rem;
                  border-top: 1px solid var(--line); }
/* The note is the reason the dialog exists — it gets the room. */
.stock-note { margin-top: .2rem; }
.stock-note-label { display: block; font-weight: 600; margin-bottom: .3rem; }
.stock-note-field { width: 100%; min-height: 11rem; resize: vertical; font: inherit;
                    line-height: 1.45; padding: .5rem .6rem; border-radius: 6px;
                    border: 1px solid var(--line); background: var(--bg); color: var(--text); }
.stock-note-field:focus { outline: none; border-color: var(--accent); }
/* Reserves its line whether or not it has text, so the field never jumps as the
   state goes empty -> Saving… -> Saved. */
.stock-note-state { margin: .3rem 0 0; min-height: 1.1em; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.badge.stop { background: var(--down); color: #2a0808; }
.sellall { margin: 0; }
.sell-row { display: flex; align-items: center; justify-content: space-between; gap: .6rem; }
.sell-row input { width: 9rem; margin: 0; }
.sellall-actions { display: flex; gap: .5rem; align-items: center; }

/* in-place position editor (issue #31) */
.pos-edit > td { background: var(--bg); }
.pos-edit-form { display: flex; align-items: flex-end; gap: .6rem; flex-wrap: wrap; padding: .2rem 0; }
.pos-edit-form label { margin: 0; color: var(--muted); font-size: .8rem; }
.pos-edit-form input { width: 6rem; margin-top: .2rem; }

/* Recommendations (issue #18) — collapsed net-action list under the cash amount */
.recs { margin-top: .8rem; border-top: 1px solid var(--line); padding-top: .7rem; }
.recs-title { font-size: .95rem; margin: 0 0 .4rem; }
.recs-none { margin: .2rem 0; }
.recs-box > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .4rem;
  border: 1px solid var(--warn); color: var(--warn); border-radius: 7px; padding: .35rem .7rem; font-weight: 600; }
.recs-box > summary::-webkit-details-marker { display: none; }
.recs-box .warn-ico { color: var(--warn); }
/* label follows open/closed state (issue #44) */
/* #70: the label text is server-rendered into data-show / data-hide so it can be
   translated — a CSS `content:` string literal never can be. */
.recs-toggle-label::after { content: attr(data-show); }
.recs-box[open] .recs-toggle-label::after { content: attr(data-hide); }
.rec-list { list-style: none; padding: 0; margin: .7rem 0 .3rem; }
.rec-row { display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  padding: .45rem .1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.rec-text { flex: 1 1 auto; }
.rec-done { flex: 0 0 auto; }
.rec-done-form { display: flex; align-items: flex-end; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
.rec-done-form[hidden] { display: none; }   /* [hidden] must beat the flex display */
.rec-price { margin: 0; color: var(--muted); font-size: .8rem; }
.rec-price input { width: 7rem; margin-top: .2rem; }

/* income/expense histogram + Monthly/Yearly tabs (#53) */
.chart-tabs { display: flex; gap: .4rem; margin: .2rem 0 .6rem; }
.chart-tab { background: transparent; border: 1px solid var(--line); color: var(--muted);
  border-radius: 7px; padding: .3rem .8rem; cursor: pointer; font: inherit; font-size: .85rem; }
.chart-tab.active { background: var(--accent); color: #0b1020; border-color: var(--accent); font-weight: 600; }
/* Dense bars: ~2px between bars, no horizontal scroll. Columns are as wide as a
   bar (client renders only as many recent periods as fit the width — #53). */
.histo { display: flex; align-items: flex-start; gap: 2px; overflow: hidden; padding: .5rem 0; min-height: 200px; }
/* min-width:0 stops a wide value label from expanding the column past 22px */
.histo-col { display: flex; flex-direction: column; align-items: center; flex: 0 0 22px; min-width: 0; }
/* centre baseline: profit grows up from the middle, loss grows down from it */
.histo-bar-wrap { height: 120px; width: 100%; position: relative; }
.histo-bar-wrap::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px dashed var(--line); }
.histo-bar { position: absolute; left: 0; right: 0; min-height: 2px; }
.histo-bar.up { bottom: 50%; background: var(--up); border-radius: 3px 3px 0 0; }
.histo-bar.down { top: 50%; background: var(--down); border-radius: 0 0 3px 3px; }
/* zero is neither profit nor loss: neutral grey stub on the baseline (#54) */
.histo-bar.zero { top: 50%; transform: translateY(-50%); background: var(--muted); border-radius: 2px; }
.histo-val.zero { color: var(--muted); }
.histo-val { font-size: .52rem; margin-top: .15rem; white-space: nowrap; width: 100%; overflow: hidden; text-align: center; }
/* #98 — the period label reads horizontally. "YYYY-MM" does not fit a 22px
   column on one line, so it is split over two ("2026" above "09"): two short
   horizontal lines, rather than one line turned on its side. Clipped like the
   value above it, so a label can never widen the column. */
.histo-label { font-size: .55rem; color: var(--muted); margin-top: .15rem;
  white-space: nowrap; letter-spacing: -.03em; line-height: 1.2;
  text-align: center; width: 100%; overflow: hidden; }
.histo-label span { display: block; }

.pairing { text-align: center; }
.code { font-size: 2rem; letter-spacing: .4rem; font-weight: 700; background: var(--bg); border: 1px dashed var(--line); border-radius: 8px; padding: .6rem; margin: .6rem 0; }
.code.sm { font-size: 1rem; letter-spacing: .1rem; padding: .2rem .4rem; }
.warn { color: var(--warn); }

/* #95 — self-approval with the account password, on the welcome screen. Above
   the pairing code and separated from it by a rule, because it is the way in
   for an account that has one and the operator's queue is the fallback. */
.pwlogin { text-align: left; }
.pwlogin h2 { margin: 0 0 .3rem; font-size: 1.05rem; }
.or-else { text-align: center; margin: 1.2rem 0 .4rem; padding-top: 1rem;
  border-top: 1px solid var(--line); }
/* the password lifecycle on the profile page; two stacked forms in one card */
.pwform { margin-top: .9rem; }
.pwform + .pwform { padding-top: .9rem; border-top: 1px solid var(--line); }
/* #107 — the link to the generator is that site's mark. Sized in `em` so it
   grows with the small print it sits in, and lifted onto the text's baseline;
   no underline, because there is no text under it to underline. */
.gen-link { display: inline-block; text-decoration: none; padding: 0 .1rem; border-radius: 5px;
            vertical-align: -.45em; line-height: 0; }
.gen-link svg { width: 1.7em; height: 1.7em; display: block; }
.gen-link:hover, .gen-link:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
.ok-note { color: var(--up); }
/* admin: the account's password row, and the tag on a self-approved session */
.pw-cell { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.badge.pw { background: var(--line); color: var(--text); }

/* #72 — session expiry countdown, above the page content on every view. Sits
   between the topbar and <main> and matches main's width, so it reads as part of
   the page rather than as a floating toast that could be missed. */
.expiry-banner {
  max-width: 920px; margin: 1rem auto -.5rem; padding: .6rem .9rem;
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  border: 1px solid var(--warn); border-radius: 10px;
  background: rgba(255,180,84,.09); color: var(--warn); font-size: .9rem;
}
.expiry-banner.danger { border-color: var(--urgent); background: rgba(255,92,92,.1); color: var(--urgent); }
.expiry-banner strong { font-weight: 700; }
.expiry-icon { font-size: 1rem; line-height: 1; }
.expiry-link { margin-left: auto; color: inherit; font-weight: 600; white-space: nowrap; }

/* ---- #71 usage guide ---- */
.guide-lede { margin-top: -.4rem; }
.guide-toc { display: flex; flex-wrap: wrap; gap: .4rem .5rem; margin: 1rem 0 .4rem; }
.guide-toc a {
  color: var(--muted); text-decoration: none; font-size: .82rem;
  border: 1px solid var(--line); border-radius: 999px; padding: .12rem .6rem;
}
.guide-toc a:hover { color: var(--accent); border-color: var(--accent); }
/* scroll-margin so a #anchor doesn't land under the top bar */
.guide-sec { scroll-margin-top: 1rem; }
.guide-sec h2 { margin-top: 0; }
.guide-dl { margin: .6rem 0 0; }
.guide-dl dt { font-weight: 600; margin-top: .9rem; }
.guide-dl dt:first-child { margin-top: 0; }
.guide-dl dd { margin: .2rem 0 0; padding-left: 1rem; color: var(--muted);
  border-left: 2px solid var(--line); }
.guide-dl dd strong, .guide-dl dd em { color: var(--text); font-style: normal; }
/* the padlocks are drawn inline in the guide, at text size */
.guide-ico { display: inline-flex; vertical-align: -.15em; }
.guide-ico svg { width: 1.05em; height: 1.05em; }
kbd {
  font: inherit; font-size: .82em; background: var(--bg); color: var(--text);
  border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px;
  padding: 0 .3em;
}
.guide-foot { margin: 1.5rem 0 0; }

/* Tables can be wide — let them scroll horizontally instead of breaking layout. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- #102 arranging the positions table ---------------------------------- */
/* The picker sits above the table as a plain disclosure, so it costs one line
   when you are not using it. It is inside the refreshed fragment, and the morph
   keeps `open` — so it does not shut itself every ten seconds. */
.col-pick { margin: .5rem 0; }
.col-pick > summary { cursor: pointer; color: var(--muted); font-size: .9rem;
                      width: max-content; }
.col-pick > summary:hover { color: var(--text); }
.col-hint { font-size: .82rem; margin: .4rem 0 .3rem; }
.col-list { list-style: none; padding: 0; margin: 0; max-width: 360px; }
/* #104 — each entry is a grip beside a two-line block: the tick, and under it
   what the value is. Aligned to the top so the grip sits on the title line and
   not halfway down the sentence. */
.col-list li { display: flex; align-items: flex-start; gap: .4rem; padding: .22rem 0; }
.col-item { display: flex; flex-direction: column; min-width: 0; }
.col-desc { font-size: .8rem; color: var(--muted); line-height: 1.3;
            padding-left: 1.35rem; }   /* line up under the label, past the box */
.col-list label { display: flex; align-items: center; gap: .4rem; margin: 0; }
/* touch-action:none so a touch-drag reorders instead of scrolling the page */
.col-handle { cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none;
              color: var(--muted); line-height: 1; padding: .15rem .2rem; border-radius: 4px; }
.col-handle:hover, .col-handle:focus { color: var(--text); outline: 1px solid var(--accent); }
.col-handle.fixed { cursor: default; opacity: .35; }
.col-list li.col-fixed label { color: var(--muted); }

/* A header is its own handle — there is no room for a grip inside a column
   title, and the whole cell is a bigger target on a phone than a glyph is.
   #103 — but that big target sits across the top of the table, so `none` here
   meant a finger that started on a heading could not scroll the page at all.
   `pan-y` gives the vertical gesture back to the browser and keeps only the
   sideways one, which is the only direction a column can go. */
.pos-table thead th[data-col]:not(.col-fixed) { cursor: grab; touch-action: pan-y;
                                                user-select: none; -webkit-user-select: none; }
.pos-table thead th[data-col]:not(.col-fixed):hover { color: var(--text); }
.pos-table thead th[data-col]:focus { outline: 1px solid var(--accent); }
.col-dragging { opacity: .85; cursor: grabbing; background: var(--panel);
                box-shadow: 0 2px 10px rgba(0,0,0,.35); }

/* ---- #105 the ticker stays on screen --------------------------------------
   The table is wider than a phone, so it scrolls sideways (.table-wrap, and the
   same after a rotation) — and the first thing to leave was the one column that
   says *which row you are reading*. Swipe left and every number is anonymous.
   So the ticker is pinned to the leading edge and the rest of the table slides
   underneath it.

   `border-collapse: separate` is not cosmetic here: with collapsed borders a
   cell's borders belong to the table rather than to the cell, so a sticky cell
   scrolls away from its own rules and the pinned column loses its lines. The
   spacing stays 0 and every line on this table is still a single `border-bottom`
   on the cell, so it looks exactly as it did.

   The background is the page's, not the cell's own — a transparent sticky cell
   is a window onto whatever is scrolling past behind it. */
.pos-table { border-collapse: separate; border-spacing: 0; }
.pos-table th[data-col="asset"], .pos-table td[data-col="asset"] {
  position: sticky; left: 0; z-index: 2; background: var(--bg);
  /* the pinned column's own edge: the only vertical rule on the table, which is
     what says the numbers are sliding under it rather than with it */
  box-shadow: 1px 0 0 var(--line);
}
/* A heading dragged across the table rides over the pinned column instead of
   disappearing behind it. */
.pos-table .col-dragging { z-index: 3; }

/* ---- #96 nothing spills over: the standing policy --------------------------
   Three causes account for practically every overflow on this page, so all
   three are ruled out globally rather than patched per widget:

   1. a word (a ticker, an account id, a URL) longer than its box;
   2. a flex or grid child, which defaults to min-width:auto and so refuses to
      shrink below its own content — the commonest one by far;
   3. something intrinsically sized (an image, a table) wider than the column.

   fit.js is the other half: it measures what still overflows after all this at
   the size the window actually is, and marks it (.is-spill / .is-scrollable). */
/* #98 — `break-word`, never `anywhere`. Both break a word that is too long for
   its line, but `anywhere` also drops the box's *min-content* width to a single
   character: a squeezed column then stops overflowing by stacking its letters
   one per line, which is what a portrait screen was showing. The owner's rule
   is the opposite — keep the letters horizontal and let the box scroll instead.
   With `break-word` the intrinsic minimum stays at the widest word, so fit.js
   measures the real overflow and can reach for the scroll below. */
body { overflow-wrap: break-word; }
img, svg, video, canvas, iframe { max-width: 100%; height: auto; }
table { max-width: 100%; }
/* Every flex/grid row on the page, named rather than starred so the rule stays
   readable — and so a future row is a deliberate addition to the list. */
:where(.topbar, nav, .crumbs, .topbar-right, .lang-switch, .row-between, .page-head,
       .pf-head, .pf-acts, .pf-top, .pf-stats, .pf-grid, .tiles, .grid, .times,
       .budget-row, .budget-edit, .budget-form, .recs-box > summary, .rec-row,
       .rec-done-form, .addline, .pos-edit-form, .sell-row, .sellall-actions,
       .perf-list li, .modal-actions, .admin-head, .user-head, .worker-grid,
       .worker-top, .worker-card, .expiry-banner, .chart-tabs, .guide-toc,
       .page-status, .note-view, .note-edit, .default-days, .tz-pick,
       .edit-grid .edit-actions, .histo, .histo-col) > * { min-width: 0; }
/* Rows that had no reason to wrap while everything fit. */
.times, .pf-top, .pf-stats, .perf-list li, .sell-row, .worker-top { flex-wrap: wrap; }
.pf-top strong { overflow-wrap: break-word; }
/* The account name is a label, not a control: it gives up width first (#70),
   at every size and not only on a phone. */
.profile-link .pfn { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A popover anchored at the left edge of its trigger can hang off the right
   edge of the window on a narrow one. */
.help-pop { max-width: min(260px, 78vw); }

/* ---- #96 the window's real shape ------------------------------------------
   The owner dragged a desktop window tall-and-narrow and the view stayed
   side-by-side: it was 700px wide, so no phone breakpoint fired, even though
   the window was plainly portrait. Orientation is the shape, independent of
   the width — a portrait window lays out as a column. The media query is the
   no-JS path; the class fit.js sets is the one that tracks a live drag. */
@media (orientation: portrait) {
  .pf-grid { grid-template-columns: 1fr; }
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .worker-card { flex-basis: 100%; }
}
html.orient-portrait .pf-grid { grid-template-columns: 1fr; }
html.orient-portrait .tiles { grid-template-columns: repeat(2, 1fr); }
html.orient-portrait .worker-card { flex-basis: 100%; }

/* ---- #96 what fit.js measured -------------------------------------------- */
/* Marked only while it genuinely overflows at the current size, and cleared at
   the start of every pass — so nothing here is sticky and nothing oscillates. */
.is-spill { min-width: 0; overflow-wrap: break-word; }
/* A cell kept on one line for the sake of tidy columns loses that when the
   alternative is pushing the page sideways. Boxes that clip or scroll on
   purpose never get here — fit.js asks the stylesheet before marking. */
.is-spill { white-space: normal; }
/* Last resort, for what cannot wrap narrow enough: scroll it. A <table> needs
   display:block before it will scroll at all. */
.is-scrollable { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
table.is-scrollable { display: block; width: max-content; max-width: 100%; }

/* ---- mobile ---- */
@media (max-width: 640px) {
  main { margin: 1rem auto; padding: 0 .8rem; }
  .topbar { flex-wrap: wrap; gap: .6rem 1rem; padding: .7rem .9rem; }
  nav { gap: .8rem; flex-wrap: wrap; }
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .grid { grid-template-columns: 1fr; }        /* stakes form: single column */
  .card.narrow { margin: 1.5rem auto; }
  table { font-size: .9rem; }
  th, td { padding: .4rem .35rem; }
  .code { font-size: 1.6rem; letter-spacing: .3rem; }
  .expiry-banner { margin: .8rem .8rem -.2rem; font-size: .85rem; }
  .topbar-right { gap: .6rem; }
  /* the account name is the first thing to give up when the bar gets tight —
     the flags and the guide are controls, the name is only a label (#70) */
  .profile-link .pfn { max-width: 6rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  input, .btn { font-size: 16px; }             /* >=16px stops iOS zoom-on-focus */
}
@media (max-width: 380px) {
  .tiles { grid-template-columns: 1fr; }
}
