/* ===== Theme tokens =====
   :root holds the "Classic" (Win95) theme - the app's only look until now.
   .pf-root[data-theme="modern"|"medieval"] below (see Estimator.prototype.
   render, which sets data-theme from Settings' theme choice) override the
   same token names with an alternative look, so every component rule that
   reads these tokens re-themes automatically with no changes of its own.
   Split two ways:
   - Structural tokens (--bevel, --radius, --font-stack, --bevel-raised/
     -sunken, --surface-shadow, --hover-bg, --chrome-border) describe *how*
     chrome is drawn - these are what actually differ between themes.
   - Palette tokens (--bg, --card, --ink, --line, ... and the mode-accent
     blocks further down: --gold, --chip-on-bg, etc.) describe *what color*
     things are. For Classic and Modern, only the neutral palette (page/
     card/text/divider colors) changes with theme - the mode-accent colors
     (session green / usage navy / both teal / files magenta) stay
     identical between the two so a mode still reads the same regardless of
     which of those is active. Medieval is a deliberate exception to that:
     it recolors the mode accents too (red/orange/gold/sepia "ink" hues
     instead of blue/green), since a theme built around parchment and
     illuminated-manuscript ink wouldn't read as medieval at all if it kept
     Windows-blue and Windows-green running through it - see the
     .pf-root.<mode>[data-theme="medieval"] blocks below the base ones. */
:root{
  --ctrl-h:34px;
  /* Win95 bevel thickness — bumped from the original 2px so the extrude in/
     out animations (grown from 0 to this value) actually read as motion
     instead of a barely-visible flicker. Ported from the mobile app's
     useExtrudeIn hook, which grows to 3px. Modern below redefines this to a
     thin flat 1px border instead of a thick two-tone one. */
  --bevel:3px;
  --font-stack:Tahoma,'MS Sans Serif',Arial,sans-serif;
  --radius:0px;
  /* Four-value border-color shorthands - a "raised" (light top/left, dark
     bottom/right) and "sunken" (inverted) bevel. Classic's is the classic
     Win95 emboss illusion; Modern's collapses both to the same flat
     single-color border, since a flat theme has no raised/sunken concept -
     pressed/active states there read through background-color instead. */
  --bevel-raised:#ffffff #000000 #000000 #ffffff;
  --bevel-sunken:#000000 #ffffff #ffffff #000000;
  --surface-shadow:none;
  --hover-bg:#fffbf0;
  --chrome-border:#000000;
  --bg:#a0a0a4; --appbar:#c0c0c0; --card:#c0c0c0; --card-head:#c0c0c0;
  --ink:#000000; --ink-soft:#000000; --muted:#000000; --faint:#808080;
  --line:#808080; --line-soft:#a0a0a4; --dash:#808080; --dash-strong:#808080;
  --input-bg:#ffffff; --input-border:#808080;
  --pill-bg:#c0c0c0; --chip-bg:#c0c0c0; --chip-border:#808080;
  --chip-on-bg:#000080; --chip-on-ink:#ffffff;
  --btn-bg:#c0c0c0; --btn-ink:#000000;
  --gold:#000080; --gold-deep:#000080; --danger:#ff0000;
  --summary-bg:#000080; --toggle-track:#808080;
  --accent:#ffffff; --accent-soft:#c0c0c0; --accent-ink:#ffffff;
  /* Dashboard quick-start tiles and Recent-Estimates snapshot tiles render
     outside any .pf-root.<mode> class (see quickStartTile/renderRecentEstimateTile
     in app.js and the `saved` mapping in rates.js), so they can't reach the
     .pf-root.session/usage/both mode-accent blocks below - these tokens are
     their own equivalent for that context. Session green / usage navy / both
     teal, matching the mode-accent colors exactly; Medieval overrides these
     too, same as it overrides --gold et al per mode. */
  --mode-session:#008000; --mode-usage:#000080; --mode-both:#008080;
  --mode-session-soft:#c0dcc0; --mode-neutral-soft:#c0c0c0;
}
/* "Modern" theme - same component CSS everywhere else, just a different
   token set. Flat borders instead of a raised/sunken bevel, rounded
   corners instead of the forced-square reset, a system sans-serif instead
   of Tahoma, a light neutral card/page palette instead of Win95 gray, and
   a soft shadow on panels for elevation instead of a bevel. Scoped to
   .pf-root (not :root) since that's the one shared wrapper every screen
   already renders inside (see Estimator.prototype.render) - the print
   document is a sibling of .pf-root, not a descendant, so it's untouched
   by this exactly like it's untouched by Classic's reset below. */
.pf-root[data-theme="modern"]{
  --font-stack:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
  --radius:10px;
  --bevel:1px;
  --bevel-raised:var(--input-border) var(--input-border) var(--input-border) var(--input-border);
  --bevel-sunken:var(--bevel-raised);
  --surface-shadow:0 1px 3px rgba(0,0,0,.10), 0 1px 2px rgba(0,0,0,.06);
  --hover-bg:#f2f2f5;
  --chrome-border:var(--line);
  --bg:#c7c7cb; --appbar:#ffffff; --card:#ffffff; --card-head:#f6f6f8;
  --ink:#1c1c1e; --ink-soft:#3a3a3c; --muted:#6b6b70; --faint:#a8a8ad;
  --line:#e4e4e8; --line-soft:#ececef; --dash:#d7d7dc; --dash-strong:#c7c7cd;
  --input-bg:#ffffff; --input-border:#d5d5da;
  --pill-bg:#eef0f2; --chip-bg:#eef0f2; --chip-border:#d5d5da;
  --btn-bg:#ffffff; --btn-ink:#1c1c1e;
}
/* "Medieval" theme - illuminated-manuscript parchment and iron-gall ink
   instead of a Windows desktop. Blackletter display type (loaded from
   Google Fonts on demand - see Estimator.ensureThemeFont - since nobody
   actually has a blackletter font installed. MedievalSharp is a
   simplified/legible blackletter cut - full Fraktur (e.g.
   UnifrakturMaguntia) reads as illegible at the small UI sizes this app
   uses everywhere - falls back to IM Fell English, a legible old-book
   serif, if the network's unavailable), a carved wood-and-gold-leaf
   bevel in place of the Win95 gray one, and a parchment background/panel
   texture (see the background-image rules further down, applied via a
   class match on this same attribute). */
.pf-root[data-theme="medieval"]{
  --font-stack:'MedievalSharp','IM Fell English',Georgia,serif;
  --radius:2px;
  --bevel:3px;
  --bevel-raised:#c9a35e #4a2c17 #4a2c17 #c9a35e;
  --bevel-sunken:#4a2c17 #c9a35e #c9a35e #4a2c17;
  --surface-shadow:0 2px 6px rgba(58,30,10,.35);
  --hover-bg:#e9d7a8;
  --chrome-border:#4a2c17;
  --bg:#e6d5a8; --appbar:#d8c28c; --card:#efe0b8; --card-head:#e3d1a0;
  --ink:#2a1a0a; --ink-soft:#3a2a1a; --muted:#5a4632; --faint:#8a7355;
  --line:#8a7355; --line-soft:#c4a876; --dash:#7a6142; --dash-strong:#5a4632;
  --input-bg:#f2e6c8; --input-border:#8a6a3a;
  --pill-bg:#e0cd9c; --chip-bg:#e0cd9c; --chip-border:#8a6a3a;
  --btn-bg:#d4bc82; --btn-ink:#2a1a0a;
  --danger:#b22222;
  /* Dashboard/Files/Settings have no mode class to accent (see the note
     below), so they'd otherwise fall through to :root's base navy - a
     jarring Windows-blue toolbar over parchment. Default to the same
     oxblood used for Session mode; the .pf-root.<mode>[data-theme=
     "medieval"] blocks below still override this per-mode via higher
     selector specificity once an estimate is actually open. */
  --chip-on-bg:#7a1414; --chip-on-ink:#f2e6c8;
  --gold:#7a1414; --gold-deep:#7a1414;
  --summary-bg:#7a1414; --toggle-track:#8a7355;
  --accent:#f2e6c8; --accent-soft:#c98a8a; --accent-ink:#f2e6c8;
  /* Dashboard quick-start tiles / Recent-Estimates snapshot tiles (see the
     --mode-* tokens on :root) - oxblood/burnt-orange/antique-gold, matching
     the .pf-root.<mode>[data-theme="medieval"] blocks below exactly, so a
     tile reads the same red/orange/gold whether you're looking at it from
     the Dashboard or from inside the open estimate. */
  --mode-session:#7a1414; --mode-usage:#8a4008; --mode-both:#8a6508;
  --mode-session-soft:#c98a8a; --mode-neutral-soft:#d9c9a0;
}
/* "Dev" theme - a deliberately plain, low-distraction look for active
   development/testing, where the Win95 bevels/mode colors and the
   Medieval parchment/blackletter are more noise than help. Same flat-
   border/no-bevel structural approach as Modern (pressed/active states
   read through background-color, not a raised/sunken illusion), but
   pushed further toward "as boring as possible": a plain system font, a
   near-white neutral gray palette tuned for contrast rather than
   character, no shadow beyond a hairline, and muted/desaturated versions
   of the mode-accent colors (still four distinct hues, so Session/Usage/
   Both/Estimates remain tellable apart at a glance, just without
   competing for attention the way saturated Windows-blue/-green do). */
.pf-root[data-theme="dev"]{
  --font-stack:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
  --radius:4px;
  --bevel:1px;
  --bevel-raised:var(--input-border) var(--input-border) var(--input-border) var(--input-border);
  --bevel-sunken:var(--bevel-raised);
  --surface-shadow:0 1px 2px rgba(0,0,0,.06);
  --hover-bg:#eef1f5;
  --chrome-border:var(--line);
  --bg:#f3f4f6; --appbar:#ffffff; --card:#ffffff; --card-head:#f8f9fb;
  --ink:#111827; --ink-soft:#374151; --muted:#6b7280; --faint:#9ca3af;
  --line:#e5e7eb; --line-soft:#f0f1f3; --dash:#d1d5db; --dash-strong:#c1c5cb;
  --input-bg:#ffffff; --input-border:#d1d5db;
  --pill-bg:#eef0f3; --chip-bg:#eef0f3; --chip-border:#d1d5db;
  --btn-bg:#ffffff; --btn-ink:#111827;
  --danger:#dc2626;
  /* Dashboard/Files/Jobs/Settings have no mode class to accent (same note
     as Medieval's) - a calm neutral slate rather than Classic/Modern's
     Windows-blue default. */
  --chip-on-bg:#475569; --chip-on-ink:#ffffff;
  --gold:#475569; --gold-deep:#3f4b5c;
  --summary-bg:#475569; --toggle-track:#d1d5db;
  --accent:#ffffff; --accent-soft:#dbe0e6; --accent-ink:#ffffff;
  --mode-session:#15803d; --mode-usage:#1d4ed8; --mode-both:#0f766e;
  --mode-session-soft:#c7e4d2; --mode-neutral-soft:#dbe0e6;
}
.pf-root.session[data-theme="dev"]{
  --chip-on-bg:#15803d; --chip-on-ink:#ffffff;
  --gold:#3f6552; --gold-deep:#33523f;
  --summary-bg:#15803d; --toggle-track:#d1d5db;
  --accent:#ffffff; --accent-soft:#c7e4d2; --accent-ink:#ffffff;
}
.pf-root.usage[data-theme="dev"]{
  --chip-on-bg:#1d4ed8; --chip-on-ink:#ffffff;
  --gold:#3d4f6b; --gold-deep:#324159;
  --summary-bg:#1d4ed8; --toggle-track:#d1d5db;
  --accent:#ffffff; --accent-soft:#c7d4f4; --accent-ink:#ffffff;
}
.pf-root.both[data-theme="dev"]{
  --chip-on-bg:#0f766e; --chip-on-ink:#ffffff;
  --gold:#3d5f5c; --gold-deep:#324e4b;
  --summary-bg:#0f766e; --toggle-track:#d1d5db;
  --accent:#ffffff; --accent-soft:#c2ded9; --accent-ink:#ffffff;
}
.pf-root.files[data-theme="dev"]{
  --chip-on-bg:#7e22ce; --chip-on-ink:#ffffff;
  --gold:#5c4f6b; --gold-deep:#4b4059;
  --summary-bg:#7e22ce; --toggle-track:#d1d5db;
  --accent:#ffffff; --accent-soft:#dfc7f4; --accent-ink:#ffffff;
}
/* Session = dark green, Usage = navy, Session & Usage (combined) = teal — a
   blend of the other two's hues — Files page = dark magenta — classic
   Windows 16-color palette hues. These fully replace the base scheme above
   once an estimate type is open, or on the Files page. Dashboard and
   Settings have no override of their own and just use :root's base navy,
   since neither one has a "mode" to accent. */
.pf-root.session{
  --chip-on-bg:#008000; --chip-on-ink:#ffffff;
  --gold:#008000; --gold-deep:#008000; --danger:#ff0000;
  --summary-bg:#008000; --toggle-track:#808080;
  --accent:#ffffff; --accent-soft:#c0dcc0; --accent-ink:#ffffff;
}
.pf-root.usage{
  --chip-on-bg:#000080; --chip-on-ink:#ffffff;
  --gold:#000080; --gold-deep:#000080; --danger:#ff0000;
  --summary-bg:#000080; --toggle-track:#808080;
  --accent:#ffffff; --accent-soft:#c0c0c0; --accent-ink:#ffffff;
}
.pf-root.both{
  --chip-on-bg:#008080; --chip-on-ink:#ffffff;
  --gold:#008080; --gold-deep:#008080; --danger:#ff0000;
  --summary-bg:#008080; --toggle-track:#808080;
  --accent:#ffffff; --accent-soft:#c0c0c0; --accent-ink:#ffffff;
}
.pf-root.files{
  --chip-on-bg:#800080; --chip-on-ink:#ffffff;
  --gold:#800080; --gold-deep:#800080; --danger:#ff0000;
  --summary-bg:#800080; --toggle-track:#808080;
  --accent:#ffffff; --accent-soft:#c0c0c0; --accent-ink:#ffffff;
}
/* Medieval's own mode-accent recolor (see the note in the theme-tokens
   comment up top for why this one breaks from Classic/Modern's "same
   accent either theme" rule) - oxblood red / burnt-umber orange / antique
   gold / iron-gall sepia in place of green/navy/teal/magenta. Higher
   specificity than the plain .pf-root.<mode> blocks above (two class
   selectors + an attribute selector beats two class selectors), so these
   win automatically whenever Medieval is the active theme - no !important
   needed. */
.pf-root.session[data-theme="medieval"]{
  --chip-on-bg:#7a1414; --chip-on-ink:#f2e6c8;
  --gold:#7a1414; --gold-deep:#7a1414;
  --summary-bg:#7a1414; --toggle-track:#8a7355;
  --accent:#f2e6c8; --accent-soft:#c98a8a; --accent-ink:#f2e6c8;
}
.pf-root.usage[data-theme="medieval"]{
  --chip-on-bg:#8a4008; --chip-on-ink:#f2e6c8;
  --gold:#8a4008; --gold-deep:#8a4008;
  --summary-bg:#8a4008; --toggle-track:#8a7355;
  --accent:#f2e6c8; --accent-soft:#d9a76a; --accent-ink:#f2e6c8;
}
.pf-root.both[data-theme="medieval"]{
  --chip-on-bg:#8a6508; --chip-on-ink:#f2e6c8;
  --gold:#8a6508; --gold-deep:#8a6508;
  --summary-bg:#8a6508; --toggle-track:#8a7355;
  --accent:#f2e6c8; --accent-soft:#d9c06a; --accent-ink:#f2e6c8;
}
.pf-root.files[data-theme="medieval"]{
  --chip-on-bg:#3d2410; --chip-on-ink:#f2e6c8;
  --gold:#3d2410; --gold-deep:#3d2410;
  --summary-bg:#3d2410; --toggle-track:#8a7355;
  --accent:#f2e6c8; --accent-soft:#b89968; --accent-ink:#f2e6c8;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);font-family:Tahoma,'MS Sans Serif',Arial,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}
a{color:var(--gold-deep);text-decoration:underline}
a:hover{color:var(--ink)}
input::placeholder,textarea::placeholder{color:var(--faint)}
input[type=number]{-moz-appearance:textfield}
input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
select option{color:#000000;background:#ffffff}

/* Reset — scoped to the app UI, not the printed estimate (.print-doc is a
   sibling of .pf-root, never a descendant, so it's untouched by this and
   keeps its own design regardless of theme). border-radius and font-family
   are theme tokens (0px/Tahoma for Classic, 10px/system-sans for Modern -
   see the token block up top); box-shadow is deliberately NOT force-reset
   here so .panel's own surface-shadow token can put a soft shadow under
   Modern's cards. */
.pf-root,.pf-root *{
  border-radius:var(--radius)!important;
  font-family:var(--font-stack)!important;
}

.appbar{background:var(--gold)!important;border-bottom:var(--bevel) solid var(--chrome-border)!important;border-radius:0!important}

/* Desktop left nav (renderSidebar in app.js) - Dashboard/Estimates/Jobs/
   Settings/Calculator, replacing the toolbar's own copies of those at
   wide viewports. Hidden below the app's existing 820px responsive
   breakpoint (see the @media block further down), where `.appbar-nav`
   (the toolbar's original button group) takes back over unchanged.
   Positioned below the toolbar in normal flow (the toolbar renders above
   .pf-shell, not inside it - see Estimator.prototype.render) and stays
   fixed in place as the page scrolls (position:sticky, set inline from
   app.js along with its `top` offset, since that has to be the toolbar's
   own measured height - see registerToolbarNode/measureToolbarHeight in
   rates.js - not a fixed value here).
   Expands/collapses on hover rather than an explicit toggle button -
   onMouseEnter/onMouseLeave (renderSidebar, app.js) flip `sidebarOpen`,
   which this class reflects the same way it always has. */
.pf-sidebar{
  flex:0 0 200px; width:200px;
  background:var(--appbar);
  border-right:var(--bevel) solid var(--chrome-border);
  border-radius:0!important;
  display:flex; flex-direction:column;
  transition:flex-basis .18s ease, width .18s ease;
}
.pf-sidebar.collapsed{flex-basis:60px; width:60px}
.pf-sidebar.collapsed .pf-sidebar-item{text-align:center; padding-left:0; padding-right:0}
/* Flat, borderless, square-cornered nav buttons - the generic
   `button{border-color:var(--bevel-raised)!important}` rule further up
   gives every button a beveled 3D edge, and the even-more-generic
   `.pf-root,.pf-root *{border-radius:var(--radius)!important}` reset
   rounds every corner - both read as stray notches on a full-width nav
   item that has to butt flush against the sidebar's own edges and its
   neighboring items above/below, hence the !important overrides here
   (and on `.pf-sidebar`/`.appbar` themselves below - anywhere two pieces
   of chrome share an edge, rounding one side looks broken, so that
   shared edge stays square regardless of theme). Selected state is a
   plain medium-grey fill with unchanged dark ink text - not `--gold`/
   white, which on light themes put white text on a near-white button
   background (the base `button{background:var(--btn-bg)}` rule is
   `!important` and beats a non-important `.active` background, so the
   grey fill below must be `!important` too to actually win). */
.pf-sidebar-item{color:var(--ink); border:none!important; border-radius:0!important}
.pf-sidebar-item:hover{background:rgba(0,0,0,.06)!important}
.pf-sidebar-item.active{background:#a0a0a0!important; color:var(--ink)!important}
/* Desktop default: sidebar visible, toolbar's own nav buttons hidden -
   flipped below 820px (see the @media block further down). */
.appbar-nav{display:none}

/* Job Details side panel (renderJobDetailPanel, app.js) - a right-side
   sliding pane, mirroring .pf-sidebar's own sticky positioning above but
   anchored to the opposite edge. Only ever mounted on the Job Detail
   screen (one specific job's fields). Uses the same `paneMinHeightPx`
   min-height as the sidebar (set inline from app.js) to stretch its own
   colored bar down to the bottom of the screen, PLUS a `max-height`
   capping it at that same edge so a long field list scrolls inside
   .pf-detail-panel-body instead of growing the panel past the viewport -
   the sidebar doesn't need that second half since its short, fixed nav
   list never overflows.
   Opened via the "Job Details" button on the Job Detail page's own header
   (renderJobDetailPage) and closed via the × button inside its own body
   (renderJobDetailPanel) - renderJobDetailPanel returns null entirely
   when closed, rather than collapsing to a thin sliver the way the
   sidebar does, since there's no persistent tab/handle on this pane to
   reopen it from. */
.pf-detail-panel{
  flex:0 0 320px; width:320px;
  background:var(--appbar);
  border-left:var(--bevel) solid var(--chrome-border);
  border-radius:0!important;
  display:flex; flex-direction:column;
  transition:flex-basis .18s ease, width .18s ease;
}
/* The scrollable field list - a sibling of the close-button header row
   (not a parent), so the sticky panel's own height stays capped by
   `max-height` (set inline from toolbarHeightPx, same source the
   sidebar's `top` uses) while just this inner list scrolls if the field
   count overflows it, same "bounded box scrolls itself" idea as
   .spreadsheet-body. */
.pf-detail-panel-body{
  flex:1 1 auto;
  min-height:0;
  overflow-y:auto;
  padding:16px 16px 16px;
}
/* Job Details autofill suggestion dropdown (jobDetailField, app.js) - a
   plain in-DOM row list, not a native <datalist> popup (which doesn't
   position itself correctly under .pf-root's `zoom` CSS - see
   jobDetailSuggestKey's doc comment in rates.js). */
.jd-suggest-row:hover{background:var(--card-head)}

.pf-in{font:600 15px/1.2 Tahoma,Arial,sans-serif;height:var(--ctrl-h);padding:0 8px;border-style:solid;border-width:var(--bevel);border-color:var(--bevel-sunken);background:var(--input-bg);color:var(--ink);outline:none;width:100%}
.pf-in:focus{border-color:var(--bevel-sunken)}
.pf-in:disabled{opacity:.6;cursor:not-allowed;background:var(--card-head)}
.pf-slot-off .pf-lbl{opacity:.6}
.pf-lbl{font:700 11px/1 Tahoma,Arial,sans-serif;letter-spacing:.03em;text-transform:uppercase;color:var(--muted)}

.dollar-box{display:flex;align-items:center;height:var(--ctrl-h);border-style:solid;border-width:var(--bevel);border-color:var(--bevel-sunken);background:var(--input-bg)}

.chip{display:inline-flex;align-items:center;gap:7px;height:var(--ctrl-h);padding:0 12px;border-style:solid;border-width:var(--bevel);border-color:var(--bevel-raised);background:var(--chip-bg);font:700 12.5px Tahoma,Arial,sans-serif;color:var(--ink);cursor:pointer;user-select:none}
.chip.on{background:var(--chip-on-bg);color:var(--chip-on-ink);border-color:var(--bevel-sunken)}

.panel{background:var(--card);border-style:solid;border-width:var(--bevel);border-color:var(--bevel-raised);box-shadow:var(--surface-shadow)}

/* Spreadsheet-style data tables (Jobs/Estimates - renderJobsPage/
   renderSavedPage in app.js) - a real grid of rows/cells with thin
   divider lines, not a stack of separately-bevelled/shadowed .panel
   "cards" the way most of this app's other lists (Recent Estimates
   tiles, performer cards) intentionally look. `.spreadsheet` is the
   sunken-bordered outer frame (the same inset look `.pf-in`/`.dollar-box`
   use for "this is a real control", here applied to the whole grid);
   `.spreadsheet-head` is the flat header bar; `.spreadsheet-row` is a
   data row - a thin bottom border between rows instead of a gap, a flat
   background instead of a raised bevel/shadow, and a hover tint since
   rows here are clickable (open that record).
   Rows keep the `.panel` class alongside `.spreadsheet-row` (not instead
   of it) purely so the existing mount/delete animations - the extrude-in
   on `.panel` and .saved-row-wrap.collapsing's shrink-out - still find
   it; `.spreadsheet-row`'s rules below are more specific (two classes)
   than plain `.panel`'s, so they win without needing !important. */
/* `.spreadsheet` is just the outer bordered frame - all scrolling (both
   axes, at once, in one scrollbar pair) happens on `.spreadsheet-body`
   inside it, which wraps BOTH the header and the rows so a horizontal
   drag moves them together in lockstep. `.spreadsheet-head` is
   `position:sticky;top:0` *inside* that same scrolling element - sticky
   only pins the axis it names, so the header still slides left/right with
   the rows on horizontal scroll, it just doesn't slide away vertically as
   the rows scroll past underneath it. (Splitting the header and rows into
   two separate scroll containers - one bounding width, the other height -
   was the earlier approach here, but overflow-x and overflow-y aren't
   fully independent: giving an element overflow-y:auto while leaving
   overflow-x at its default `visible` computes an effective overflow-x of
   `auto` too per the CSS overflow spec, so that second container grew its
   own horizontal scrollbar in addition to the frame's - two scrollbars for
   what's supposed to be one movement.) */
.spreadsheet{
  border-style:solid;
  border-width:var(--bevel);
  border-color:var(--bevel-sunken);
  background:var(--card);
  border-radius:0!important;
}
.spreadsheet-body{
  display:flex;
  flex-direction:column;
  max-height:60vh;
  overflow:auto;
}
.spreadsheet-head{
  display:flex;
  background:var(--card-head);
  border-bottom:2px solid var(--line);
  border-radius:0!important;
  position:sticky;
  top:0;
  z-index:1;
}
.spreadsheet-row.panel{
  border-style:solid;
  border-width:0 0 1px 0;
  border-color:var(--line);
  box-shadow:none;
  background:var(--card);
  border-radius:0!important;
}
.spreadsheet-row.panel:hover{background:var(--card-head)}

/* Job Detail page's in-page tab strip (Estimates/Timeline/Talent Roster -
   see renderJobDetailPage, app.js) - the first tab set anywhere in this
   app, so there's nothing else to match; flat bottom-bordered buttons
   sitting on the strip's own border-bottom, active tab filled/underlined
   the same way the left sidebar highlights its own active item. */
.jd-tab{
  font:600 13px 'IBM Plex Sans';
  color:var(--muted);
  background:transparent;
  border:none;
  border-bottom:2px solid transparent;
  margin-bottom:-2px;
  padding:9px 14px;
  cursor:pointer;
  border-radius:0;
}
.jd-tab:hover{color:var(--ink);background:var(--card-head)}
.jd-tab-active{color:var(--ink);border-bottom-color:var(--gold)}

/* Timeline tab's calendar grid (renderTimelineCalendar, app.js) - plain
   CSS Grid month view, 7 columns, no calendar library. `.cal-cell` sizes
   itself off aspect-ratio rather than a fixed height, so it scales with
   the app's own `zoom` CSS the same way every other panel here does. */
.cal-grid{
  display:grid;
  grid-template-columns:repeat(7,1fr);
  gap:1px;
  background:var(--line);
  border:1px solid var(--line);
}
.cal-dow{
  background:var(--card-head);
  font:700 11px 'IBM Plex Sans';
  text-transform:uppercase;
  letter-spacing:.04em;
  color:var(--muted);
  text-align:center;
  padding:6px 0;
}
.cal-cell{
  background:var(--card);
  min-height:78px;
  padding:4px 5px;
  display:flex;
  flex-direction:column;
  gap:2px;
  /* Without this, a grid item's automatic minimum width defaults to its
     content's min-content size - an unbroken (white-space:nowrap) event
     label longer than the column would otherwise force this whole
     column (and every other cal-cell sharing it) wider to fit, instead
     of just being clipped/ellipsized by .cal-event-chip's own
     overflow:hidden as intended. This is the standard fix. */
  min-width:0;
}
.cal-cell-empty{background:var(--bg)}
.cal-day-num{font:700 11px 'IBM Plex Mono';color:var(--muted)}
.cal-event-chip{
  font:600 10px 'IBM Plex Sans';
  color:#ffffff;
  border-radius:3px;
  padding:1px 5px;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

/* A usage_cycle event's continuous bar (cal.bars/renderCalendarBar,
   rates.js/app.js) - a SEPARATE grid item placed directly via
   grid-row/grid-column (one per contiguous run of days within a single
   calendar row, spanning that many day-columns), not nested inside
   .cal-cell - so it sits at the grid-line boundaries between days with
   no padding of its own to fight, and min-width:0 keeps a long label
   from forcing the columns it spans any wider (same reasoning as
   .cal-cell's own min-width:0 above). align-self:end anchors it near
   the bottom of its row, clear of the day number and any single-day
   chips stacked above it in the day cell(s) it overlaps. Rounded only
   at the cycle's true start/end; a 'mid' run is a plain rectangle so it
   reads as a straight-through continuation rather than a new bar. */
.cal-event-bar{
  font:600 10px 'IBM Plex Sans';
  color:#ffffff;
  padding:1px 5px;
  margin-bottom:3px;
  align-self:end;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
/* !important on all four - .pf-root's own reset (.pf-root,.pf-root *
   {border-radius:var(--radius)!important}, near the top of this file)
   otherwise forces every run back to the theme's uniform corner radius,
   which would round every run equally and erase the one-continuous-bar
   look these four rules exist to create. */
.cal-bar-solo{border-radius:3px!important}
.cal-bar-start{border-radius:3px 0 0 3px!important}
.cal-bar-end{border-radius:0 3px 3px 0!important}
.cal-bar-mid{border-radius:0!important}

/* Parchment texture — pure CSS, no image assets or extra network requests:
   layered mottled radial-gradients blended over the base parchment color
   to suggest aged, unevenly-stained paper. Applied to the page background
   and, more subtly, to .panel "sheets" so cards read as separate pieces of
   parchment rather than a flat color swatch. app.js sets the page's base
   color inline as background:var(--bg) (a background-color once the
   shorthand resolves) - background-image is a different sub-property, so
   this layers on top of that rather than fighting over the same one; the
   !important here is just to also win over .panel's own plain
   background:var(--card) shorthand for the same reason. */
.pf-root[data-theme="medieval"]{
  background-image:
    radial-gradient(ellipse at 12% 18%, rgba(90,55,20,.18), transparent 42%),
    radial-gradient(ellipse at 88% 12%, rgba(90,55,20,.14), transparent 46%),
    radial-gradient(ellipse at 78% 88%, rgba(70,40,10,.20), transparent 50%),
    radial-gradient(ellipse at 8% 92%, rgba(70,40,10,.16), transparent 46%),
    radial-gradient(ellipse at 50% 50%, rgba(90,55,20,.06), transparent 70%)
  !important;
  background-blend-mode:multiply;
}
.pf-root[data-theme="medieval"] .panel{
  background-image:
    radial-gradient(ellipse at 25% 15%, rgba(90,55,20,.10), transparent 45%),
    radial-gradient(ellipse at 80% 85%, rgba(90,55,20,.08), transparent 50%)
  !important;
  background-blend-mode:multiply;
}

.pf-mobilebar{display:none}
.print-doc{display:none}

/* Buttons: uniform Win95 raised gray with an explicit white/black bevel
   (the browser's automatic outset/inset shading was too subtle to read as
   a highlight), sunken look while pressed. Overrides the per-button inline
   colors/borders that predate this theme. */
/* display:flex + centering guarantees the label sits centered on both axes
   regardless of line-height/font-metric quirks - a plain block/inline-block
   button only centers horizontally via text-align, and leaves vertical
   position to whatever slack the text's line box happens to leave above vs.
   below, which isn't reliably symmetric across browsers/fonts. */
button{font-family:inherit;display:flex;align-items:center;justify-content:center;border-style:solid!important;border-width:var(--bevel)!important;border-color:var(--bevel-raised)!important;background:var(--btn-bg)!important;cursor:pointer}
button:active{border-color:var(--bevel-sunken)!important}
button:hover{background:var(--hover-bg)!important}
.btn-reset:hover{color:var(--ink)!important}
.chip:not(.on):hover{background:var(--hover-bg)}

/* Consistent control height so inputs/dollar fields/chips/buttons line up
   when side by side. Excludes the dashboard's quick-start tiles and the
   vertical Compare tab (deliberately taller/standalone, or in the tab's
   case sized by its stacked letters). */
button:not(.quick-tile):not(.btn-dash-lg):not(.tab-v){height:var(--ctrl-h)!important}

/* Session mode's floating "+ Add Performer" / "Compare" / Close buttons —
   see renderSessionCastStack in app.js. Plain square Win95 raised bevel
   (the global button reset already gives that), just positioned above and
   beside the performer stack rather than living in the app bar. */
.tab-h{
  padding:0 14px;
  font:700 11.5px 'IBM Plex Sans';
  letter-spacing:.03em;
  text-transform:uppercase;
}
/* Compare button + (while comparing) its Close button, stacked together
   beside the card — Close sits just under Compare. */
.tab-v-group{
  position:absolute;
  left:100%;
  top:0;
  margin-left:10px;
  display:flex;
  flex-direction:column;
  gap:8px;
}
.tab-v{
  width:var(--ctrl-h);
  padding:16px 0;
  writing-mode:vertical-rl;
  text-orientation:upright;
  font:700 11px 'IBM Plex Sans';
  letter-spacing:.1em;
  line-height:1;
}
.tab-close-v{
  width:var(--ctrl-h);
  height:var(--ctrl-h);
  padding:0;
  display:flex;
  align-items:center;
  justify-content:center;
  color:var(--danger);
  font:700 15px/1 'IBM Plex Sans';
  writing-mode:horizontal-tb;
}

/* Newly-mounted performer cards grow+fade in — see @starting-style below.
   Existing cards further up the stack aren't remounted when a new one is
   appended, so this only plays for a card that's genuinely new to the DOM.
   position:relative (no z-index) makes this a "positioned" element so it
   stacks by DOM order against the also-positioned .tab-v (which renders
   earlier — see renderSessionCastStack) instead of a positioned element
   automatically winning over static content regardless of source order. */
.perf-card-wrap{
  position:relative;
  display:grid;
  grid-template-rows:1fr;
  opacity:1;
  transition:grid-template-rows 420ms ease-out, opacity 300ms ease-in 260ms;
}
.perf-card-wrap > .panel{min-height:0}
@starting-style{
  .perf-card-wrap{
    grid-template-rows:0fr;
    opacity:0;
  }
}
/* Removing a performer card (see Estimator.remove — ported from the mobile
   app's useCollapseOnExit hook): collapse height to 0 the same way the
   entrance grows it, plus a matching negative margin-bottom to cancel out
   the 18px flex `gap` in renderCastStack's stack so no gap-shaped sliver is
   left behind once the card is actually removed from the array. */
.perf-card-wrap.collapsing{
  grid-template-rows:0fr!important;
  opacity:0!important;
  margin-bottom:-18px;
  transition:grid-template-rows 280ms ease-in, opacity 200ms ease-in, margin-bottom 280ms ease-in;
  pointer-events:none;
}

/* Usage rows (renderUsageRow) get the same grow-in/collapse-out treatment
   as performer cards, wrapped one level in since the row itself is a flex
   layout, not the thing whose height should animate. */
.usage-row-wrap{
  display:grid;
  grid-template-rows:1fr;
  opacity:1;
  transition:grid-template-rows 260ms ease-out, opacity 220ms ease-in 140ms;
}
.usage-row-wrap > div{min-height:0;overflow:hidden}
@starting-style{
  .usage-row-wrap{ grid-template-rows:0fr; opacity:0; }
}
.usage-row-wrap.collapsing{
  grid-template-rows:0fr!important;
  opacity:0!important;
  transition:grid-template-rows 240ms ease-in, opacity 160ms ease-in;
  pointer-events:none;
}

/* Saved Estimates (and Jobs) rows collapse out the same way when deleted
   (see Estimator.deleteSaved/deleteJob) — no matching entrance since the
   list just loads in on page-open, not one row at a time. */
.saved-row-wrap{
  display:grid;
  grid-template-rows:1fr;
  opacity:1;
  transition:grid-template-rows 280ms ease-in, opacity 200ms ease-in;
}
.saved-row-wrap > .panel{min-height:0;overflow:hidden}
.saved-row-wrap.collapsing{
  grid-template-rows:0fr!important;
  opacity:0!important;
  margin-bottom:-10px;
  transition:grid-template-rows 280ms ease-in, opacity 200ms ease-in, margin-bottom 280ms ease-in;
  pointer-events:none;
}

/* "Extrude in" — a newly-mounted element grows into place while its
   background fades in from the page background, instead of just popping in.
   Scoped to the same element set mobile's useExtrudeIn hook wired this
   into (Win95Button/Panel/Chip/LabeledInput/LabeledSelect/DollarField ↔
   button/.panel/.chip/.pf-in/.dollar-box here) rather than every div, so
   decorative 1px dividers and pill-shaped buttons that were never part of
   the bevel system are left alone. Plays automatically on whatever
   genuinely mounts fresh - no per-callsite class needed.

   This animates transform:scale() rather than border-width. An earlier
   version grew border-width from 0 - box-sizing:border-box keeps that
   symmetric only when an element has an explicit width AND height; most of
   these are sized by their own content (button text, chip labels), so the
   growing border pushed the box's far edge outward while its near edge sat
   fixed by normal-flow layout, reading as growth anchored to one corner.
   transform is a paint-time effect with no layout participation at all - it
   always scales from the exact center of the box's final geometry
   regardless of how that geometry was determined, so this can't recur. */
button, .chip, .panel, .pf-in, .dollar-box{
  transition:transform 260ms cubic-bezier(.2,1.4,.4,1), background-color 260ms ease-out, opacity 220ms ease-out;
}
@starting-style{
  button, .chip, .panel, .pf-in, .dollar-box{
    transform:scale(.82);
    background-color:var(--bg)!important;
    opacity:0;
  }
}

/* "Extrude out" — the reverse, for the containers above that already do a
   two-phase removal (collapsing/.collapse): shrink the removed element back
   toward its center in step with its height/opacity collapse, instead of it
   just clipping away instantly. */
.perf-card-wrap.collapsing > .panel,
.saved-row-wrap.collapsing > .panel{
  transform:scale(.82);
  background-color:var(--bg)!important;
  transition:transform 260ms ease-in, background-color 260ms ease-in;
}

/* Confirm dialogs (Navigation / Delete) extrude out the same way instead of
   vanishing the instant Yes/No is clicked - see confirmLeaveYes/No and
   confirmDeleteYes/No's brief *Closing state. */
.dialog-closing{
  transform:scale(.82);
  background-color:var(--bg)!important;
  opacity:0;
  transition:transform 220ms ease-in, background-color 220ms ease-in, opacity 200ms ease-in;
}

/* Compare column two-phase close (see Estimator.closeCompareColumn): fade
   first, then collapse width/padding so the remaining column(s) grow to
   fill the freed space, only removing the column from state once both
   steps have finished. */
.compare-col{
  transition:opacity 180ms ease-in, flex-basis 200ms ease-in, min-width 200ms ease-in, margin-left 200ms ease-in, padding-left 200ms ease-in;
}
.compare-col.fade{ opacity:0; }
.compare-col.collapse{
  opacity:0;
  flex-basis:0!important;
  min-width:0!important;
  flex-grow:0!important;
  margin-left:0!important;
  padding-left:0!important;
  border-left-width:0!important;
  overflow:hidden;
  pointer-events:none;
}

.icon-btn{width:var(--ctrl-h)!important;padding:0!important}
button:focus-visible,.chip:focus-visible,select:focus-visible,input:focus-visible{outline:1px dotted #000000;outline-offset:1px}

@media (max-width:820px){
  .est2{grid-template-columns:minmax(0,1fr)!important;padding-bottom:92px!important}
  .est2 aside{position:static!important;top:auto!important}
  .pf-in{font-size:16px!important}
  .pf-mobilebar{display:flex!important}
  .proj-grid{grid-template-columns:1fr!important}
  .cardhead-row{flex-wrap:wrap!important;row-gap:11px!important}
  .cat-sel{flex:1 1 100%!important;width:100%!important;min-width:0!important;order:-1}
  .dup-btn{padding:9px 12px!important}
  .appbar{flex-wrap:wrap!important;row-gap:11px!important}
  .pf-sidebar{display:none!important}
  .pf-detail-panel{display:none!important}
  .appbar-nav{display:flex!important}
}
@media (max-width:560px){
  .appbar{padding:11px 14px!important;gap:10px!important}
}
@media print{
  .pf-root,.pf-mobilebar{display:none!important}
  .print-doc{display:block!important}
  .print-preview-bar{display:none!important}
  .print-preview-page-root{background:none!important;min-height:0!important;display:block!important}
  .print-preview-page{padding:0!important;overflow:visible!important;display:block!important}
  .print-zoom-wrap{zoom:1!important}
  .print-preview-page .print-doc{box-shadow:none!important;width:auto!important;padding:0!important;background:none!important}
  html,body{background:#ffffff!important}
  @page{size:letter portrait;margin:0.6in}
}

/* Print Preview - its own full page (renderPrintPreviewPage in app.js;
   Estimator.prototype.render branches to just this, skipping .pf-root
   entirely) rather than a dialog stacked over the estimate, with an
   editable rendering of the print document and its own zoom (the
   .print-zoom-wrap the zoom % actually applies to, kept separate from
   .print-preview-page's own scroll/centering so zooming doesn't fight the
   layout). Deliberately theme-independent (fixed dark bar, plain white
   page) - the printed estimate has its own look (printTheme in app.js)
   regardless of which Classic/Modern/Medieval theme is active in the rest
   of the app. Zoom is reset to 1!important at print time (@media print
   above) so the printed output is never affected by the on-screen zoom. */
.print-preview-page-root{min-height:100vh;display:flex;flex-direction:column;background:#3a3a3c}
.print-preview-bar{flex:0 0 auto;display:flex;align-items:center;gap:14px;padding:12px 20px;background:#1c1c1e;position:sticky;top:0;z-index:10}
.print-preview-page{flex:1 1 auto;overflow:auto;padding:40px 20px;display:flex;justify-content:center;align-items:flex-start}
.print-preview-page .print-doc{display:block!important;background:#ffffff;box-shadow:0 8px 40px rgba(0,0,0,.5);padding:0.6in;box-sizing:border-box;width:8.5in;max-width:none}
/* Real, separate page sheets stacked with a visible gap between them,
   instead of one continuously-tall document - see renderPaginatedPrintDoc/
   layoutPrintPages (single estimate) and renderPaginatedCompareDoc/
   layoutComparePages (compare mode, paginated per column independently).
   Each sheet keeps a min-height (not a fixed one) so a lightly-filled last
   page still looks like a full sheet, without ever clipping content that
   runs long. Compare mode with more than 2 columns uses .landscape sheets
   (11x8.5in) instead of the default portrait 8.5x11in - matching
   pdfgen.js's own choice for the actual PDF, since several columns need
   the extra width far more than the extra height. */
.print-pages-stack{display:flex;flex-direction:column;align-items:center;gap:36px}
.print-page-sheet .print-doc{min-height:11in}
.print-page-sheet.landscape .print-doc{width:11in;min-height:8.5in}
.print-preview-page [contenteditable="true"]:hover{outline:1px dashed #999!important}
.print-preview-page [contenteditable="true"]:focus{outline:1px solid #1565c0!important;background:rgba(21,101,192,.06)}
/* Notes field's placeholder - CSS-only (:empty/:before), so it's never
   part of the field's actual text and can't get captured as a real
   "note" reading literally "Click to add notes..." if left untouched. */
.print-notes:empty:before{content:'Click to add notes…';color:#999;font-style:italic}
@media print{ .print-notes-block:has(.print-notes:empty){display:none} }
