@font-face {
  font-family: "SansPro";
  font-display: swap;
  src: url('https://d122f4822rob2u.cloudfront.net/fonts/SourceSansPro-Regular.ttf');;
}

@font-face {
  font-family: "SansProBold";
  font-display: swap;
  src: url('https://d122f4822rob2u.cloudfront.net/fonts/SourceSansPro-SemiBold.ttf');;
}

@font-face {
  font-family: "SansProLight";
  font-display: swap;
  src: url('https://d122f4822rob2u.cloudfront.net/fonts/SourceSansPro-ExtraLight.ttf');;
}

@font-face {
  font-family: "righteous";
  font-display: swap;
  src: url('https://d122f4822rob2u.cloudfront.net/fonts/Righteous-Regular.ttf');;
}

@font-face {
  font-family: "avenirBold";
  font-display: swap;
  src: url('https://d122f4822rob2u.cloudfront.net/fonts/Ubuntu-Medium.ttf');;
}





html{
  scroll-padding-top: 90px;
}

.boldBody{
  font-family: 'SansProBold', sans-serif;
  font-size: 1.1em;
}

.lightBody{
  font-family: 'SansPro', sans-serif;
  font-size: 1.05em;
}


.nav-link:hover{
  background-color: #e8edf2 !important; /* steel grey */
}

.navbar .nav-link.active {
  font-weight: normal !important; /* Prevent bold font on active link */
  background-color: #e8edf2 !important; /* steel grey */
}

.navbar .nav-item {
  font-weight: normal !important; /* Prevent bold font on active link */
}

.darkhover {
  -webkit-filter: brightness(100%);
  cursor: pointer;
}

.darkhover:hover {
  -webkit-filter: brightness(80%);
  filter: brightness(80%);
  -webkit-transition: all 1s ease;
  -moz-transition: all 1s ease;
  -o-transition: all 1s ease;
  -ms-transition: all 1s ease;
  transition: all 1s ease;
}

    .register_holder {
      display: flex;
      align-items: center;
      justify-content: center;
    }

.btn-outline-light:hover{
  color: black !important;
}

.btn-outline-light:focus{
  color: black !important;
}

.fixed_alert{
  position: fixed;
  top: 0px;
  left: 50%;
  transform: translate(-50%, 0%);
  width: 92%;
  animation: slidedown 0.2s 0.2s ease-out forwards;
}
@keyframes slidedown {
  to {
    top: 47px;
  }
}

  @media only screen and (min-width: 800px) {  
    .w400 {
      font-weight: 400;
      font-size: 1.6em;
    }  
  }

  @media only screen and (max-width: 800px) {  
    .w400 {
      font-weight: 400;
      font-size: 1.2em;
    }  
  }
  

  @media only screen and (min-width: 800px) {  
    .w300 {
      font-weight: 400;
      font-size: 1.4em;
    }  
  }

  @media only screen and (max-width: 800px) {  
    .w300 {
      font-weight: 400;
      font-size: 1.2em;
    }  
  }  


  @media only screen and (min-width: 800px) {     
    .w800 {
      font-weight: 800;
      font-size: 3.5em;
    }
  }

  @media only screen and (max-width: 800px) {     
    .w800 {
      font-weight: 800;
      font-size: 2.5em;
    }
  }


.white{
color:white;
}


.selected{
  background-color: #E5E4E4;
  border: 3px solid black;
}

.selected5{
  background: white !important;
  /* make button look selected */
  border: 1px solid black !important;
  color: black !important;
  /* shadow */
  box-shadow: 0 0 10px rgba(0,0,0,0.5);
}


.font-primary {
  background-image: linear-gradient(to right, var(--tw-gradient-stops));
  color: transparent; /* Make the text color transparent */
  background-clip: text; /* Clip the background to the text */
  -webkit-background-clip: text; /* For Safari */
  --tw-gradient-from: #9CACFF;
  --tw-gradient-to: rgb(214 173 255 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), #D6ADFF, var(--tw-gradient-to);
  --tw-gradient-to: #D7B8FF;
}

.font-primary-darker {
  background-image: linear-gradient(to right, var(--tw-gradient-stops));
  color: transparent; /* Make the text color transparent */
  background-clip: text; /* Clip the background to the text */
  -webkit-background-clip: text; /* For Safari */
  --tw-gradient-from: #1a3a5c;
  --tw-gradient-to: rgb(46 109 164 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), #2e6da4, var(--tw-gradient-to);
  --tw-gradient-to: #2e6da4;
}


.font-primary-dark{
  color: #1a3a5c !important;
}


.btn-primary {
  color: white;
  background-color: #1B5FA8;
  border: 1px solid #164D8A;
}

.text-primary {
  color: #1D4ED8 !important; /* mirror .btn-primary bg */
}


.btn-primary:hover {
  background-color: #164D8A;
  border: 1px solid #164D8A;
}


.btn-primary:focus{
  background-color: #164D8A;
  border: 1px solid #164D8A;
}

.btn-primary.disabled{
  background-color: rgb(88, 88, 88) !important;
  border: 1px solid black !important;
}

.btn-primary:disabled{
  background-color: rgb(88, 88, 88) !important;
  border: 1px solid black !important;
}


/* -----------------------------------------------------------
   OUTLINE VARIANT THAT VISUALLY MATCHES YOUR .btn-primary
   -----------------------------------------------------------*/

/* default (transparent background, coloured text/border) */
.btn-outline-primary {
  color: #1B5FA8;
  background-color: transparent;
  border: 1px solid #1a3a5c;
  border-radius: 4px;
}

/* hover / focus / active — match the filled style’s hover */
.btn-outline-primary:hover,
.btn-outline-primary:focus,
.btn-outline-primary:active,
.btn-outline-primary.active,
.btn-outline-primary.dropdown-toggle.show {
  color: #000;                 /* accessible contrast  */
  background-color: #e8edf2;   /* steel grey hover */
  border-color: #1a3a5c;
  text-decoration: none;       /* kill link underline in <a> buttons */
}

/* focus ring for keyboard users (Bootstrap normally handles this,
   but re-assert here in case your global CSS resets it) */
.btn-outline-primary:focus-visible {
  outline: 0;
  box-shadow: 0 0 0 0.25rem rgba(26, 58, 92, 0.25); /* 4 px focus halo */
}

/* disabled states (match your filled button’s grey) */
.btn-outline-primary:disabled,
.btn-outline-primary.disabled {
  color: rgb(88, 88, 88);     /* muted text for contrast */
  background-color: transparent;
  border-color: rgb(88, 88, 88);
  pointer-events: none;
  opacity: 0.65;              /* keeps parity with Bootstrap defaults */
}


.btn-outline-dark{
  background-color: white
}

.btn-outline-dark:hover{
  color:black !important;
  background-color: #e8edf2; /* steel grey */
  border: 1px solid black;
}





.form-control{
  border-color: black;
}

.form-select {
  text-align: center;
}

@media only screen and (min-width: 1200px) {
  .pt-10 {
    padding-top: 120px;
  }
}
@media only screen and (max-width: 1200px) {
  .pt-10 {
    padding-top: 30px;
  }
}


.results {
  max-width: 2000px;
}

footer {
  position: fixed;
  bottom: 0;
  width: 100%;
}

/* Bootstrap 5.3.3's .nav-link declares font-size:var(--bs-nav-link-font-size), a custom
   property 5.3.3 never actually defines -- invalid at computed-value time, so these anchors
   inherit the <li>'s full body size (17.6px) and the .small class on them in layout.html is
   inert (.nav-link is declared later than .small at equal specificity, so it wins). Restore
   the intended small size explicitly, matching the footer's own address/copyright text. */
footer .nav-link.small {
  font-size: .875em;
}

.viewportheight {
  min-height:87vh;
}

.predictorcolour {
 background-color: #f4f6f8;
}

.curveborder {
  border-radius: 10px;
}

.fade-in-slow2{
  -webkit-animation: 0.2s ease 0s normal forwards 1 fadein;
  animation: 0.2s ease 0s normal forwards 1 fadein;
  opacity:1
}

.fade-in-slow3{
  -webkit-animation: 1s ease 0s normal forwards 1 fadein;
  animation: 1s ease 0s normal forwards 1 fadein;
  opacity:1
}

@keyframes fadein{
  0% { opacity:0; }
  75% { opacity:0; }
  100% { opacity:1; }
}

@-webkit-keyframes fadein{
  0% { opacity:0; }
  75% { opacity:0; }
  100% { opacity:1; }
}

.fade-in-fast{
  -webkit-animation: 0.1s ease 0s normal forwards 1 fadein;
  animation: 0.1s ease 0s normal forwards 1 fadein;
  opacity:1
}

@keyframes fadein{
  0% { opacity:0; }
  33% { opacity:0; }
  100% { opacity:1; }
}

@-webkit-keyframes fadein{
  0% { opacity:0; }
  33% { opacity:0; }
  100% { opacity:1; }
}

.rotate {
  transition: 0.70s;
  -webkit-transition: 0.70s;
  -moz-transition: 0.70s;
  -ms-transition: 0.70s;
  -o-transition: 0.70s;
}
/*
.flash {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 2.5s;
}



.flash1 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 2.6s;
}

.flash2 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 2.7s;
}

.flash3 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 2.8s;
}

.flash4 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 2.9s;
}

.flash5 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 0.1s;
}

.flash6 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 0.2s;
}

.flash7 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 0.3s;
}

.flash8 {
  animation-name: glowing;
  animation-duration: 0.2s; 
  animation-iteration-count: 1;
  animation-play-state: running; 
  animation-timing-function: ease-out; 
  animation-delay: 0.4s;
}
*/

.dropdown-menu {
  max-height: 400px;
  overflow-y: auto;
}

  .fade_in_fast{
    -webkit-animation: 0.5s ease 0s normal forwards 1 fadein;
    animation: 0.5s ease 0s normal forwards 1 fadein;
    opacity:1
  }

@media only screen and (max-width: 500px) {
  iframe[src*=youtube] {
    width: 330px;
    height: 200px;
  }
}

.initial_hidden{
  display: none;
}

a[type='button'] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}


.index_title{
  /* font family bold maven pro */
  font-family: 'avenirBold', sans-serif;
  letter-spacing: 0.02em;
}

/* Override inline-block styling for CKEditor */
.django-ckeditor-widget {
  display: block !important; /* Change to block */
  width: 100%;              /* Full width */
}





.canvas_align{
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translate(-50%, -50%);               
}

/* For mobile devices */

/* .database_holder{
  min-height: 350px;
} */

b{
  font-family: 'SansProBold', sans-serif;
  font-size: 1em;
}

.visually-hidden {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}


@media (max-width: 992px) {
  /* Style for screens smaller than "lg" */
}

.modal-content{
  border-radius: 10px;
}

.card{
  border-radius: 10px;
}

.card-header{
  background: none;
  border-radius: 10px;
}

.card-footer{
  background: none;
  border-radius: 10px;
}

/* Add this if you want to differentiate user and AI messages */

.min-vh-100{
  min-height: calc(100vh - 52px) !important;
}

/* #titleholder {
  display: flex;
  align-items: flex-end;
  justify-content: center;
} */

@media (max-width: 1200px) { /* This corresponds to Bootstrap's 'md' breakpoint */
}


.bg_light_gradient {
  background: linear-gradient(300deg,#f5f5f5,#d3ced8,#e9e9e9);
  background-size: 180% 180%;
  animation: gradient-animation 24s ease infinite;
}

@keyframes gradient-animation {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}


/* if screensize is less then 800px then regs_list height is 3vh if greater than this then 60vh */



/* .btn-design-menu removed -- was only used by design_tools_card.html's
   vertical category menu, replaced by .dt-pill below. The bare `.btn:active`
   half of its old shared selector is sitewide (every Bootstrap button's
   active/pressed state), kept standalone so removing the dead class rule
   doesn't change that. */
.btn:active{
    background-color: #e3e2e0;
    color: #28242c;
    font-weight: bold;
}

/* ─── FREE DESIGN TOOLS (Phase 4 CalcForge-dashboard-pattern port) ───
   Light-mode pill switcher + compact icon+name tile grid for
   /free-civil-engineering-design-software (design_tools_card.html).
   Same layout shape as CalcForge's dashboard .cf-tool-pill/.cf-dash-tool-*
   (a different, dark-mode project) but built from this site's own tokens
   and .topic-chip pattern (blog/static/blog/blog-nav.css) rather than
   copying those dark values. */
.dt-panel{
  /* Extra bottom padding (was 22px all round) now that .dt-footnote is gone --
     the tile grid/rail is the last thing in the card, so it gets more breathing
     room instead of ending flush with the card edge. */
  background:#fff; border:1px solid var(--line); border-radius:18px; padding:22px 24px 40px;
}
/* Wraps .dt-panel on both hubs (estimating_hub.html, design_tools.html) --
   a plain class instead of Bootstrap's .pb-5 so this can be tuned per-page
   without fighting .pb-5's !important (see Feature-page Hero Backgrounds
   note in CLAUDE.md re: utility classes silently winning over overrides). */
.dt-tools-section{ padding-bottom:96px; }
/* Fold wrapper -- hero + tools section on both hubs (estimating_hub.html,
   design_tools.html). min-height, never height: on a narrow screen the tile
   grid drops to 1 column and the card grows taller than the fold, which must
   simply be allowed to happen -- this is a floor, not a cap. --fold-offset is
   the distance from the viewport top to this wrapper (the static .site-nav,
   plus the breadcrumb bar on the estimating hub only, since only that view
   passes `breadcrumbs` into context); each page sets its own value in its
   {% block extra_css %}. svh, not vh: on mobile vh measures the viewport with
   browser chrome retracted, which would push the FAQ further down than
   intended once the chrome reappears. */
.dt-fold{
  display:flex; flex-direction:column;
  min-height: calc(100vh  - var(--fold-offset, 62px));   /* fallback for no svh support */
  min-height: calc(100svh - var(--fold-offset, 62px));
}
.dt-fold > .dt-tools-section{ display:flex; flex-direction:column; flex:1 1 auto; }
/* .dt-panel must itself be a flex column, not just a flex-grown block, or
   its own child (.dt-panel-body) has no flex parent to stretch against --
   flex:1 1 auto on .dt-panel-body below would be inert and the extra height
   would just sit as blank space under .dt-panel-body, inside .dt-panel's own
   box, never reaching the rail/tile-grid at all. */
.dt-fold > .dt-tools-section > .dt-panel{ display:flex; flex-direction:column; flex:1 1 auto; }
/* Stretch the rail's vertical divider the full card height so the empty
   space below a short category's tiles reads as intentional sidebar space,
   not a layout gap. Scoped to .dt-fold so .dt-panel-body's default
   align-items:start (below) is untouched anywhere else it's used. Tiles stay
   top-aligned at their natural height -- only the rail and its border-right
   stretch, never .dt-tile-grid's rows. */
/* align-content:stretch grows the grid's single row track itself to fill
   .dt-panel-body's flex-grown height -- align-items:stretch alone only
   stretches .dt-rail/.dt-tile-grid to that row's natural (content) height,
   which would leave the extra space sitting unfilled below the row instead
   of reaching the rail/divider. */
.dt-fold .dt-panel-body{ flex:1 1 auto; align-items:stretch; align-content:stretch; }
.dt-panel-head{ display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:16px; }
.dt-panel-head h2{ margin:0; }
.dt-tool-count{
  font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase;
  color:var(--mute); background:var(--bg-2); border-radius:999px; padding:4px 12px; white-space:nowrap;
}
/* Category rail (category rail redesign, Free Tools Pages.dc.html design
   import) -- replaces .dt-pill-row/.dt-pill on both hubs. .dt-panel-body
   is the 216px-rail / flexible-grid layout; the rail itself is a plain
   vertical nav, active item highlighted with an inset left bar matching
   the accent-underline convention used elsewhere (e.g. the estimating-tool
   mode toggle's .selected state). This is one of two .dt-* media queries in
   this codebase (the other, just below, steps the tile grid down from 3 to
   2 to 1 columns) -- below ~768px the rail collapses to a horizontal
   wrapping row above the grid instead of a left column, since a fixed
   216px column doesn't have room at that width. */
.dt-panel-body{ display:grid; grid-template-columns:216px 1fr; gap:24px; align-items:start; }
/* border-right (rather than a plain border) would run flush from the very
   top to the very bottom of the stretched .dt-fold rail -- the ::after
   pseudo-element below is inset top/bottom instead, so the divider reads as
   a deliberate rule inside the card, not an edge-to-edge column boundary. */
.dt-rail{ position:relative; display:flex; flex-direction:column; gap:2px; padding-right:20px; }
.dt-rail::after{ content:""; position:absolute; top:6px; bottom:6px; right:0; width:1px; background:var(--line); }
.dt-rail-eyebrow{
  font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.14em; text-transform:uppercase;
  color:var(--mute); padding:2px 10px 10px;
}
.dt-rail-item{
  display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%; text-align:left;
  cursor:pointer; border:0; background:transparent; border-radius:8px; padding:9px 10px;
  font-family:'IBM Plex Sans',system-ui,sans-serif; font-size:13.5px; font-weight:500; color:var(--ink-2);
  transition:background .15s, color .15s;
}
.dt-rail-item:hover{ background:rgba(27,95,168,.04); }
.dt-rail-item.selected{ background:rgba(27,95,168,.08); color:var(--accent); font-weight:600; }
.dt-rail-count{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; color:var(--mute); }
.dt-rail-item.selected .dt-rail-count{ color:var(--accent); }
.dt-tile-grid{ display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; align-content:start; }
@media (max-width:767.98px){
  .dt-panel-body{ grid-template-columns:1fr; }
  .dt-rail{ flex-direction:row; flex-wrap:wrap; gap:6px; border-bottom:1px solid var(--line); padding:0 0 14px; }
  .dt-rail::after{ display:none; }
  .dt-rail-eyebrow{ display:none; }
  .dt-rail-item{ width:auto; border:1px solid var(--line); border-radius:999px; padding:7px 14px; }
  .dt-rail-item.selected{ border-color:var(--accent); }
  .dt-tile-grid{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
  /* Rail is now a horizontal pill row above the grid, not a left column --
     the full-height stretch above only makes sense for the two-column
     desktop layout. */
  .dt-fold .dt-panel-body{ align-items:start; }
}
@media (max-width:479.98px){
  .dt-tile-grid{ grid-template-columns:1fr; }
}
.dt-tile{
  display:flex; align-items:center; gap:11px; background:var(--bg); border:1px solid var(--line);
  border-radius:11px; padding:12px 13px; text-decoration:none; color:inherit; transition:background .15s, border-color .15s;
  min-height:75px;
}
.dt-tile:hover{ background:rgba(27,95,168,.06); border-color:var(--accent); color:inherit; }
.dt-tile-icon{
  flex:none; width:34px; height:34px; border-radius:9px; background:var(--bg-2);
  display:flex; align-items:center; justify-content:center;
}
.dt-tile-icon img{ width:18px; height:18px; }
.dt-tile-name{ flex:1; min-width:0; font-size:13.5px; font-weight:600; color:var(--navy); line-height:1.25; }

/* Per-category AI-bridge CTA tile (design import, Free Tools Pages.dc.html
   round 2) -- the last tile in each category's grid on both hubs, replacing
   the single generic band that used to sit below the whole tools card
   (estimating hub's video band, design hub's .dt-learn card -- both removed).
   A .dt-tile modifier, not a separate component: reuses .dt-tile's box/flex/
   gap/radius/padding. Was a solid navy tile with a "Civils.ai · paid" /
   "CalcForge · course" kicker line and a trailing arrow -- now styled like
   every other tile (same background/border/text/icon-badge colour, no
   arrow) so it doesn't read as a jarring block in the grid, with a small
   accent "Premium" badge in the corner carrying the same signal the kicker
   used to. .dt-tile-cta-icon only exists to colour the Font Awesome glyph
   (var(--accent), matching the site's other icon-in-circle pattern, e.g.
   the nav dropdown's .dd-ico) -- every other tile's icon is a pre-coloured
   img with nothing to tint. */
.dt-tile-cta{ position:relative; }
.dt-tile-cta-icon{ color:var(--accent); }
.dt-tile-cta-copy{ flex:1; min-width:0; display:flex; flex-direction:column; }
.dt-tile-cta-label{ font-size:13.5px; font-weight:600; color:var(--navy); line-height:1.25; }
.dt-tile-cta-badge{
  position:absolute; top:8px; right:9px;
  font-family:'IBM Plex Mono',monospace; font-size:8.5px; letter-spacing:0.08em; text-transform:uppercase;
  color:#fff; background:var(--accent); border-radius:999px; padding:2px 8px; line-height:1.4;
}

/* Tool-tile entrance -- both free-tools hubs. Fires on first paint and again
   on every category switch, because .d-none is display:none and a hidden
   element restarts its animation when shown (no JS involved on either hub).
   Delay comes from --dt-i, the tile's index WITHIN ITS OWN CATEGORY:
   nth-child supplies it below on the design hub (one grid per category);
   the estimating hub sets it inline per tile instead, since its single
   #est-tile-grid holds every category's tiles at once and nth-child there
   would count across all categories, not just the visible one. */
.dt-tile-grid > .dt-tile{
  animation: dt-tile-in .32s cubic-bezier(.22,.61,.36,1) both;
  animation-delay: calc(var(--dt-i, 0) * 30ms);
}
@keyframes dt-tile-in{
  from{ opacity:0; transform:translateY(-10px); }
  to  { opacity:1; transform:none; }
}
/* Design hub only -- its five grids are id="<cat>_buttons", one per
   category, so DOM position already IS the per-category index. Scoped by
   the id suffix so this can never reach #est-tile-grid, where it would be
   wrong (that grid's tiles get --dt-i inline from the template instead). */
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(1){ --dt-i:0; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(2){ --dt-i:1; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(3){ --dt-i:2; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(4){ --dt-i:3; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(5){ --dt-i:4; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(6){ --dt-i:5; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(7){ --dt-i:6; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(8){ --dt-i:7; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(9){ --dt-i:8; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(10){ --dt-i:9; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(11){ --dt-i:10; }
.dt-tile-grid[id$="_buttons"] > .dt-tile:nth-child(n+12){ --dt-i:11; }

@media (prefers-reduced-motion: reduce){
  .dt-tile-grid > .dt-tile{ animation:none; }
}



.li-cross {
  position: relative;
  padding-left: 1.5em;  /* space to preserve indentation on wrap */
}

.li-cross::before{
  content: "✗";
  position: absolute;
  left: 0;
  color: red;
  
}


body {
  max-width: 100vw;
}

.form-check-input:checked {

  background-color: #1a3a5c;
  border-color: #000;
}

.spinner-border{
  overflow: hidden;
}

strong{
  font-family: 'SansProBold', sans-serif;
  font-size: 1.1em;
}

a{
  color: #1a3a5c
}

.bg_dark_gradient{
  background: rgb(35,37,37);
  background: linear-gradient(135deg, rgba(35,37,37,1) 0%, rgba(39,39,39,1) 100%);
  border: 1px solid rgb(70, 70, 70);
}

.card-hover-animation:hover {
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* Add shadow */
  background-color: #e8edf2 !important;
}

/* ===== MODERN ANIMATIONS ===== */

/* ---- Hero section ---- */
.hero-badge {
  display: inline-block;
  font-size: 0.8rem;
  font-weight: 500;
  color: #1B5FA8;
  background: rgba(27, 95, 168, 0.08);
  border: 1px solid rgba(27, 95, 168, 0.2);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  letter-spacing: 0.01em;
}

.hero-highlight {
  color: #1B5FA8;
  position: relative;
}
.hero-highlight::after {
  content: '';
  position: absolute;
  bottom: -3px; left: 0; right: 0;
  height: 3px;
  background: #1B5FA8;
  border-radius: 2px;
  opacity: 0.5;
}

.hero-chip {
  display: inline-block;
  font-size: 0.78rem;
  color: #495057;
  background: #fff;
  border: 1px solid #dee2e6;
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
}

.hero-video-glow {
  box-shadow: 0 8px 40px rgba(27, 95, 168, 0.16), 0 2px 12px rgba(0, 0, 0, 0.06);
}

.btn-outline-secondary {
  border-radius: 8px !important;
}

/* Card hover lift */
.card-lift {
  transition: transform 0.22s ease, box-shadow 0.22s ease;
}
.card-lift:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
}

/* Typewriter cursor */
.txt-rotate > .wrap {
  border-right: 2px solid #1B5FA8;
  padding-right: 2px;
  animation: blink-cursor 0.7s step-end infinite;
}
@keyframes blink-cursor {
  0%, 100% { border-color: #1B5FA8; }
  50%       { border-color: transparent; }
}


.background1{
  background-color: #f4f4f2;
}

html{
  font-family: 'SansPro', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 1.1em;
  line-height: 1.65;
}

h2, h3 {
  font-weight: 600;
  line-height: 1.3;
}

.pricing{
  font-size: 0.9em !important;
}

/* CSS for making all table columns equal width */
table.equal-width-columns th, 
table.equal-width-columns td {
  width: 25%; /* Divide the table width equally among 4 columns */
}

/* Optional: Ensure table layout is fixed to respect the width */
table.equal-width-columns {
  table-layout: fixed;
  width: 100%; /* Ensure the table spans the full container width */
}

.btn-outline-purple{
  border: 1px solid #1a3a5c !important;
}

.dropdown-menu-center {
  left: 50% !important;
  right: auto !important;
  text-align: center !important;
  transform: translate(-50%, 0) !important;
}


.btn-group .btn {
  font-weight: normal;
  text-decoration: none;
  color: black;
  background: none;
  border: none; /* Remove all borders initially */
}

.btn-group .btn-check:checked + .btn {
  font-weight: bold;
  color: black;
  background: none;
  border-bottom: 3px solid black !important; /* Force thicker black border */
}

.btn-group .btn-outline-primary {
  border: none; /* Remove Bootstrap's outline styles */
  border-radius: 0px;
  padding-left: 25px;
  padding-right: 25px;

}


.category-red{
  background-color: #FDEBEC;
}

.category-grey{
  background-color: #F1F1EF;
}

.category-blue{
  background-color: #E9F3F7;
}

.category-brown{
  background-color: #F3EEEE;
}

.category-green{
  background-color: #EDF3EC
}

.workflow-card{
  cursor: pointer;
  text-decoration: none;
}

.workflow-card:hover{
  border-color: black;
  /* background-color: #eeefff; */
}

.workflow-card:hover .card-header{
  background-color: #e8edf2 !important;
}

.badge-rotate {
  display: inline-block;
  opacity: 1;
  transition: opacity 0.5s ease-in-out;
}

.badge-rotate.hide {
  opacity: 0;
}


.check-list {
  list-style: none; /* Remove default bullets */
  margin: 0;
  padding: 0;
}

.check-list li {
  position: relative;
  margin-bottom: 0.5em;
  padding-left: 1.5em;      /* Space for the icon */
  line-height: 1.3;         /* Slightly increase line spacing */
}

/* Use Font Awesome’s “content” for the check icon */
.check-list li::before {
  font-family: "Font Awesome 5 Free"; /* Adjust if you use another FA version/name */
  content: "\f00c";                  /* \f00c is the "check" icon */
  font-weight: 900;                  /* Solid icon weight (fa-solid) */
  color: #4CAF50;                    /* Green color—customize as needed */
  position: absolute;
  left: 0;
  top: 0;                            /* Align near the top of the line */
  font-size: 1.2em;                  /* Make the icon larger/bolder */
}


.marquee {
  --gap: 2rem;
  display: flex;
  overflow: hidden;
  user-select: none;
  gap: var(--gap);
}

.marquee__content,
.marquee__content2 {
  flex-shrink: 0;
  display: flex;
  justify-content: space-around;
  min-width: 100%;
  gap: var(--gap);
  animation: scroll 38s linear infinite;
}

.marquee__content img,
.marquee__content2 img {
  width: 160px;
  filter: grayscale(100%);
  opacity: 0.55;
  transition: filter 0.3s ease, opacity 0.3s ease;
}

.marquee__content img:hover,
.marquee__content2 img:hover {
  filter: grayscale(0%);
  opacity: 1;
}

.marquee__content li,
.marquee__content2 li {
  list-style: none;
}

.marquee-outer {
  position: relative;
}

.marquee-outer::before,
.marquee-outer::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  width: 100px;
  z-index: 2;
  pointer-events: none;
}

.marquee-outer::before {
  left: 0;
  background: linear-gradient(to right, #F7F6F3 0%, transparent 100%);
}

.marquee-outer::after {
  right: 0;
  background: linear-gradient(to left, #F7F6F3 0%, transparent 100%);
}

.bg-light .marquee-outer::before {
  background: linear-gradient(to right, #f8f9fa 0%, transparent 100%);
}

.bg-light .marquee-outer::after {
  background: linear-gradient(to left, #f8f9fa 0%, transparent 100%);
}

@keyframes scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(calc(-100% - var(--gap)));
  }
}

/* ─── TRUST STRIP — infinite logo marquee ───────────────────────────────── */
/* Shared component: tunnelsai/templates/tunnelsai/_logo_marquee.html, included by
   index.html and about.html. .marquee/.marquee__content/.marquee-outer above have
   generic base rules; every selector below is scoped under .trust so it wins on
   specificity and can override what's inherited. */
.trust{ padding:32px 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); background:var(--bg-2); }
.trust .lbl{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; color:var(--mute); max-width:180px; flex-shrink:0; }
.trust .lbl b{ color:var(--navy); display:block; font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:15px; text-transform:uppercase; margin-bottom:2px; }

.trust .marquee-outer{
  flex:1 1 0; min-width:0; /* without min-width:0 the max-content track below forces the row wider than the container */
  overflow:hidden;
  -webkit-mask:linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
  mask:linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}
.trust .marquee-outer::before,
.trust .marquee-outer::after{ content:none; } /* neutralise the global solid-colour edge fades -- this strip uses a mask instead */
.trust .marquee{ display:flex; width:max-content; gap:24px; animation:trust-marquee 60s linear infinite; }
.trust .marquee-outer:hover .marquee{ animation-play-state:paused; }
.trust .marquee__content{
  display:flex; align-items:center; gap:24px;
  list-style:none; margin:0; padding:0;
  min-width:0; justify-content:flex-start; animation:none; /* override the global .marquee__content's own min-width:100%/animation/justify-content */
}
/* padding-block gives the hover lift's translateY(-2px) somewhere to go --
   .marquee-outer's overflow:hidden (required for the mask) has zero
   headroom otherwise and clips the top of every logo on hover */
.trust .marquee__content li{ display:flex; flex-shrink:0; padding-block:4px; }
.trust .marquee__content img{
  height:75px; width:auto; max-width:none; object-fit:contain;
  aspect-ratio:2/1; /* every logo file is a 300x150 canvas -- reserves the correct width before the image loads, so the track's max-content size (and the loop math below) is right from first paint */
  opacity:.82; filter:grayscale(100%) contrast(.95);
  transition:opacity .25s, filter .25s, transform .25s;
}
.trust .marquee__content img:hover{ opacity:1; filter:none; transform:translateY(-2px); }
@keyframes trust-marquee{
  from{ transform:translateX(0); }
  to{ transform:translateX(calc(-50% - 12px)); } /* -50% - gap/2: see docs/plans notes -- wrapper gap adds a half-gap the naive -50% misses */
}

@media (max-width: 767.98px){
  .trust .container-fluid{ flex-direction:column; align-items:flex-start; }
  .trust .lbl{ max-width:none; }
  /* flex-direction:column flips the main axis, so the desktop flex:1 1 0 (a
     row-axis rule) would set a *height*-basis of 0 here instead -- with no
     extra vertical space to grow into, that collapses the track to 0px tall.
     Override to a fixed-size column item instead of a flex-growing one. */
  .trust .marquee-outer{ flex:0 0 auto; width:100%; }
  .trust .marquee__content img{ height:67px; }
}

@media (prefers-reduced-motion: reduce){
  .trust .marquee{ animation:none; }
  .trust .marquee-outer{ overflow-x:auto; -webkit-mask:none; mask:none; }
  /* the aria-hidden duplicate track exists only to loop the animation -- with
     the animation off it would just be a second, redundant scroll of the same
     15 logos. Removing it also lets .marquee's own width:max-content correctly
     shrink to the one remaining track's natural width. */
  .trust .marquee__content[aria-hidden="true"]{ display:none; }
}

.youtube2 {
  position: relative;
  padding-bottom: 65%; /* matches embedded video aspect ratio */
  height: 0;
}

.youtube2 iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}


#compare-civils-chatgpt .py-6 { padding-top: 3.5rem !important; padding-bottom: 3.5rem !important; }
  #compare-civils-chatgpt .py-7 { padding-top: 4.5rem !important; padding-bottom: 4.5rem !important; }

  #compare-civils-chatgpt .bg-gradient-primary {
    background-image: linear-gradient(135deg, #1a3a5c 0%, #2e6da4 100%);
  }
  #compare-civils-chatgpt .bg-gradient-dark {
    background-image: linear-gradient(135deg, #111 0%, #444 100%);
  }
  #compare-civils-chatgpt .brand-dot {
    width: 36px; height: 36px; border-radius: 12px;
    box-shadow: 0 6px 16px rgba(0,0,0,0.15);
  }

  #compare-civils-chatgpt .table > :not(caption) > * > * {
    padding-top: .95rem; padding-bottom: .95rem;
  }

  #compare-civils-chatgpt .table-section td {
    background: rgba(0,0,0,.02);
    font-size: .8rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #6c757d;
    font-weight: 600;
  }
  #compare-civils-chatgpt .section-label::before{
    content: "";
    display: inline-block;
    width: .5rem; height: .5rem;
    background: #1a3a5c; border-radius: 50%;
    margin-right: .5rem; vertical-align: middle;
    box-shadow: 0 0 0 3px rgba(26,58,92,.15);
  }

  /* Subtle “soft” badges using Bootstrap vars where available */
  #compare-civils-chatgpt .bg-primary-subtle { background-color: rgba(13,110,253,.12) !important; color: #0d6efd !important; }
  #compare-civils-chatgpt .bg-success-subtle { background-color: rgba(25,135,84,.12) !important; color: #198754 !important; }
  #compare-civils-chatgpt .bg-secondary-subtle { background-color: rgba(108,117,125,.12) !important; color: #6c757d !important; }
  #compare-civils-chatgpt .bg-dark-subtle { background-color: rgba(33,37,41,.08) !important; color: #212529 !important; }
  #compare-civils-chatgpt .bg-body-tertiary { background-color: rgba(0,0,0,.03) !important; }

/* ════════════════════════════════════════════════════════════════════════════
   DESIGN SYSTEM ("Direction B" / Steel-blue editorial)
   Shared across: index.html, about.html, cad_ai.html, geotech_ai.html,
   workflow_specs_search.html. Page-specific overrides remain in each
   template's {% block extra_css %}.
   ════════════════════════════════════════════════════════════════════════════ */

/* ─── Palette tokens ─────────────────────────────────────────────────────── */
:root{
  --bg:#f4f4f2; --bg-2:#e9e8e3;
  --ink:#1a1a1a; --ink-2:#3a3733; --mute:#7a766e; --line:#d8d6cf;
  --navy:#1a1a1a; --navy-2:#2e2c28;
  /* Signal Indigo -- --accent/--accent-2 are the light-surface pair (text,
     links, button fill). --accent-lift/--accent-tint are dark-band-only:
     a flat --accent on #1a1a1a fails contrast (was 2.69:1), so dark bands
     get a lifted fill (paired with --navy ink text, not white) and a
     lifted text tint instead of reusing the light pair directly. */
  --accent:#1B5FA8; --accent-2:#154a85;
  --accent-lift:#7587F3; --accent-tint:#92A4FC;
  --orange:var(--accent); --orange-2:var(--accent-2);
  --ok:#16a34a;
  --bs-body-bg: var(--bg);
  --bs-body-color: #0e1116;
  --bs-body-font-family: 'IBM Plex Sans', system-ui, sans-serif;
  --bs-border-color: var(--line);
}

/* ─── Container width override ───────────────────────────────────────────
   Bootstrap 5.3 caps .container-xxl (and friends) at 1320px starting at
   the xxl breakpoint (≥1400px). We bump that to 1440px so wide-desktop
   displays use the full editorial canvas the templates were designed
   against. The trigger MUST be 1400px (not 1440) — at 1400-1439px the
   Bootstrap cap would otherwise still squeeze content to 1320 and the
   hero H1 wraps onto an extra line. */
@media (min-width: 1400px){
  .container,
  .container-sm,
  .container-md,
  .container-lg,
  .container-xl,
  .container-xxl{ max-width:1440px; }
}

.design-b{ font-family:'IBM Plex Sans', system-ui, sans-serif; font-size:15px; line-height:1.55; color:#0e1116; background:var(--bg); }
.design-b ::selection{ background:var(--accent); color:#fff; }
.design-b a{ color:inherit; text-decoration:none; }

/* ─── Type system ────────────────────────────────────────────────────────── */
.ff-display{ font-family:'Saira Semi Condensed',sans-serif; text-transform:uppercase; letter-spacing:-0.01em; }
.ff-mono   { font-family:'IBM Plex Mono',monospace; }
.display-h1{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:62px; line-height:1.05; letter-spacing:-0.01em; color:var(--navy); text-transform:uppercase; }
.display-h2{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:56px; line-height:1.03; letter-spacing:-0.015em; color:var(--navy); text-transform:uppercase; margin:0; }
.display-h3{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:30px; line-height:1.08; color:var(--navy); text-transform:uppercase; }
/* Subtle vertical gradient on em-highlighted words. Lifted accent at the
   top, full accent at the bottom — reads cleanly on the cream body bg and
   on the dark navy bands (.uc.primary, .edit-band, .sec-final). Falls back
   to solid accent in browsers without background-clip:text. */
.display-h1 em, .display-h2 em, .display-h3 em{
  font-style:normal; font-weight:800;
  background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 70%, #fff), var(--accent));
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
  color:var(--accent);
}
/* Dark-band override -- the light gradient above is built from --accent, which
   fails contrast on #1a1a1a. Rebuild the same gradient from --accent-tint instead.
   Must override the whole gradient, not just `color`: a bare color override loses
   to -webkit-text-fill-color:transparent above and silently does nothing (this
   was the actual bug behind .edit-band's now-removed dead override). */
.sec-final .display-h1 em, .sec-final .display-h2 em, .sec-final .display-h3 em,
.edit-band .display-h1 em, .edit-band .display-h2 em, .edit-band .display-h3 em,
.uc.primary .display-h1 em, .uc.primary .display-h2 em, .uc.primary .display-h3 em{
  background:linear-gradient(180deg, color-mix(in srgb, var(--accent-tint) 70%, #fff), var(--accent-tint));
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
  color:var(--accent-tint);
}
.lede{ font-size:17px; color:var(--ink-2); max-width:56ch; }

.kicker{ font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:0.04em; color:var(--accent); font-weight:500; display:flex; align-items:center; gap:10px; }
.kicker::before{ content:""; width:24px; height:1px; background:var(--accent); }
.kicker-line{ font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--navy); letter-spacing:0.06em; display:flex; align-items:center; gap:12px; }
.kicker-line::before{ content:""; width:32px; height:1px; background:var(--navy); }

/* ─── Buttons ────────────────────────────────────────────────────────────── */
.design-b .btn-navy{ background:var(--navy); color:#fff; border:0; padding:16px 28px; border-radius:4px; font-weight:600; font-size:13px; letter-spacing:0.06em; text-transform:uppercase; display:inline-flex; align-items:center; gap:10px; }
.design-b .btn-navy:hover{ background:#000; color:#fff; }
/* .btn-orange's only render site (index.html's final CTA) sits on the dark
   .sec-final band, so unlike .btn-accent below it doesn't need a scoped dark
   override -- it's dark-only, period. Lifted fill + ink text per the design
   study's own default treatment. */
.design-b .btn-orange{ background:var(--accent-lift); color:var(--navy); border:0; padding:20px 36px; border-radius:4px; font-weight:600; font-size:14px; letter-spacing:0.06em; text-transform:uppercase; display:inline-flex; align-items:center; gap:10px; }
.design-b .btn-orange:hover{ background:color-mix(in srgb, var(--accent-lift) 82%, black); color:var(--navy); }
.design-b .btn-accent{ background:var(--accent); color:#fff; border:0; padding:18px 32px; border-radius:4px; font-weight:600; font-size:13px; letter-spacing:0.06em; text-transform:uppercase; display:inline-flex; align-items:center; justify-content:center; gap:10px; transition:background .15s; }
.design-b .btn-accent:hover{ background:var(--accent-2); color:#fff; }
/* .btn-accent is dual-context: correct as-is on every light surface (incl. the
   hero CTAs it's newly used for), but on the dark bands it already rendered on
   before this change (.sec-final / .edit-band / .est-bridge-card) the base fill
   above fails contrast just like .btn-orange did -- same lifted-fill+ink fix,
   scoped so light usages are untouched. */
.sec-final .btn-accent, .edit-band .btn-accent, .est-bridge-card .btn-accent{
  background:var(--accent-lift); color:var(--navy);
}
.sec-final .btn-accent:hover, .edit-band .btn-accent:hover, .est-bridge-card .btn-accent:hover{
  background:color-mix(in srgb, var(--accent-lift) 82%, black); color:var(--navy);
}
.design-b .btn-outline-navy{ border:1.5px solid var(--navy); color:var(--navy); background:transparent; padding:12px 20px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:0.04em; text-transform:uppercase; font-weight:500; display:inline-flex; align-items:center; gap:10px; }
.design-b .btn-outline-navy:hover{ background:var(--navy); color:#fff; }
.design-b .uc.primary .btn-outline-navy{ border-color:rgba(255,255,255,.5); color:#fff; }
.design-b .uc.primary .btn-outline-navy:hover{ background:#fff; color:var(--navy); border-color:#fff; }

/* Hero CTA entrance -- plays once on page load, never loops (explicit ask).
   Hooked via an explicit .hero-cta class on the 7 hero buttons (homepage,
   cad_ai, geotech_ai, workflow_specs_search) rather than a .feature-hero
   descendant selector: that base class is on 11 templates (also
   compare_detail.html, cost_data_hub.html, cost_data_slice.html), and a
   selector scoped to it would have dragged those in too.
   fill-mode is BACKWARDS, not both/forwards -- a filling animation outranks
   regular declarations in the cascade, so forwards/both would leave
   transform:none stuck on .btn-accent forever and permanently break
   index.html's own hover-lift override (below, .btn-accent:hover{
   transform:translateY(-2px) }). backwards only fills during the delay,
   before the animation starts, and hands the element back to the normal
   cascade the moment it ends -- safe here because the last keyframe is
   already opacity:1;transform:none, so there's no jump at handoff. */
.hero-cta{
  animation: hero-cta-in .38s cubic-bezier(.22,.61,.36,1) backwards;
  animation-delay: .12s;
}
/* Secondary outline button on the 3 feature-page heroes -- adjacent sibling
   of the primary in the same .d-flex row, so no per-page selector needed.
   The homepage's lone CTA has no following .hero-cta sibling and simply
   keeps the .12s delay above. */
.hero-cta + .hero-cta{ animation-delay: .2s; }

@keyframes hero-cta-in{
  from{ opacity:0; transform:translateY(10px); }
  to  { opacity:1; transform:none; }
}

@media (prefers-reduced-motion: reduce){
  .hero-cta{ animation:none; }
}

/* ─── Hero video frame (used on homepage hero + geotech borehole image) ─── */
.hero-video{ position:relative; border-radius:16px; overflow:hidden; background:var(--navy); box-shadow:0 40px 80px -30px rgba(11,42,72,.35); }
.hero-video video, .hero-video img{ width:100%; height:auto; display:block; }
.corner-tag{ position:absolute; top:16px; left:16px; background:rgba(11,42,72,.88); color:#fff; font-family:'IBM Plex Mono',monospace; font-size:11px; padding:6px 10px; border-radius:4px; display:flex; align-items:center; gap:8px; }
.dot-live{ width:6px; height:6px; background:var(--orange); border-radius:50%; }

/* ─── Feature-page hero (cad_ai, geotech_ai, workflow_specs_search) ─────── */
.feature-hero{ position:relative; padding:0 16px; background:var(--bg); }
@media (max-width: 991.98px){ .feature-hero{ padding:0 16px; } }
.fh-eyebrow{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.18em; color:var(--accent); text-transform:uppercase; display:inline-flex; align-items:center; gap:14px; padding:24px 0 0; }
.fh-eyebrow::before, .fh-eyebrow::after{ content:""; width:14px; height:1px; background:var(--accent); display:inline-block; }
.fh-meta-row{ display:flex; align-items:center; justify-content:space-between; padding:16px 0 36px; border-bottom:1px solid var(--line); margin-bottom:48px; flex-wrap:wrap; gap:16px; }
.fh-meta-row .lhs{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:13px; letter-spacing:0.06em; color:var(--mute); text-transform:uppercase; }
.fh-meta-row .lhs b{ color:var(--navy); }
.fh-exhibit{ position:relative; padding:14px; background:#fff; border:1px solid var(--line); border-radius:14px; box-shadow:0 30px 60px -28px rgba(11,42,72,.18); }
.fh-exhibit-head{ display:flex; align-items:center; justify-content:space-between; padding:0 4px 12px; flex-wrap:wrap; gap:8px; }
.fh-exhibit-frame{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.14em; color:var(--mute); text-transform:uppercase; }
.fh-exhibit-frame b{ color:var(--navy); font-weight:600; }
.fh-exhibit-cap{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.06em; color:var(--mute); }
.fh-exhibit-cap .dot{ width:5px; height:5px; background:#10b981; border-radius:50%; display:inline-block; margin-right:6px; vertical-align:middle; box-shadow:0 0 0 3px rgba(16,185,129,.18); }
.fh-exhibit-video{ position:relative; border-radius:8px; overflow:hidden; background:#000; }
.fh-exhibit-video video, .fh-exhibit-video img{ width:100%; height:auto; display:block; }

/* ─── Feature-hero dark variant ─────────────────────────────────────────── */
.feature-hero--dark{ background:transparent; border-top-color:rgba(255,255,255,.15); isolation:isolate; }
.feature-hero--dark::before{
  content:"";
  position:absolute; left:50%; top:0;
  transform:translateX(-50%);
  width:100vw; height:100%;
  background:var(--navy);
  z-index:-1;
}
.feature-hero--dark .display-h1{ color:#fff; }
.feature-hero--dark .display-h1 em{ background:linear-gradient(180deg, color-mix(in srgb, var(--accent-tint) 70%, #fff), var(--accent-tint)); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
.feature-hero--dark .lede{ color:rgba(255,255,255,.65); }
.feature-hero--dark .fh-eyebrow{ color:var(--accent-tint); }
.feature-hero--dark .fh-eyebrow::before,
.feature-hero--dark .fh-eyebrow::after{ background:var(--accent-tint); }
.feature-hero--dark .btn-navy{ background:#fff; color:var(--navy); box-shadow:0 12px 28px -16px rgba(0,0,0,.5); }
.feature-hero--dark .btn-outline-navy{ border-color:rgba(255,255,255,.35); color:#fff; }
.feature-hero--dark .btn-outline-navy:hover{ background:rgba(255,255,255,.08); border-color:rgba(255,255,255,.6); }

/* ─── Use-case cards ─────────────────────────────────────────────────────── */
.uc{ background:#fff; border:1px solid var(--line); border-radius:20px; padding:36px; height:100%; display:flex; flex-direction:column; }
.uc.primary{ background:var(--navy); border-color:var(--navy); color:#fff; }
.uc.primary .display-h3{ color:#fff; }
.uc.primary .lede, .uc.primary p{ color:rgba(255,255,255,.78); }
.uc-num{ font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--accent); }
.uc-tag{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; color:var(--mute); padding-left:14px; border-left:1px solid var(--line); }
.uc.primary .uc-tag{ color:rgba(255,255,255,.55); border-left-color:rgba(255,255,255,.15); }
.uc.primary .uc-num{ color:var(--accent-tint); }
.uc-bullets{ list-style:none; padding:0; margin:0 0 28px; }
.uc-bullets li{ display:flex; gap:12px; padding:8px 0; font-size:14px; color:var(--ink-2); }
.uc.primary .uc-bullets li{ color:rgba(255,255,255,.8); }
.uc-bullets li i{ color:var(--accent); font-size:12px; margin-top:6px; }
.uc.primary .uc-bullets li i{ color:var(--accent-tint); }
.uc-bullets li b{ color:var(--navy); font-weight:600; }
.uc.primary .uc-bullets li b{ color:#fff; }
.uc-video{ position:relative; border-radius:12px; overflow:hidden; background:#000; aspect-ratio:16/10; box-shadow:0 20px 50px -25px rgba(0,0,0,.3); }
.uc-video video{ width:100%; height:100%; display:block; object-fit:cover; }
.overlay-tag{ position:absolute; top:14px; left:14px; background:rgba(11,42,72,.92); color:#fff; font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.04em; padding:5px 10px; border-radius:3px; text-transform:uppercase; z-index:2; }
.uc.primary .overlay-tag{ background:rgba(201,125,46,.92); }

/* ─── Edit / export band ─────────────────────────────────────────────────── */
.edit-band{ background:var(--navy); color:#fff; border-radius:24px; padding:48px; }
@media (max-width: 991.98px){ .edit-band{ padding:32px; } }
.edit-band .display-h2{ color:#fff; }
/* .edit-band .display-h2 em's dark-context gradient override now lives with
   the other .display-h* em rules above (a bare `color` here always lost to
   -webkit-text-fill-color:transparent on the base rule -- this line used to
   be silently dead). */
.edit-band p{ color:rgba(255,255,255,.78); }
.edit-band ul{ list-style:none; padding:0; margin:0 0 28px; }
.edit-band ul li{ display:flex; gap:12px; padding:8px 0; font-size:14px; color:rgba(255,255,255,.85); align-items:flex-start; }
.edit-band ul li i{ color:var(--accent-tint); margin-top:5px; font-size:11px; }
.edit-band .uc-video{ box-shadow:0 30px 60px -25px rgba(0,0,0,.6); }

/* ─── How-it-works steps ─────────────────────────────────────────────────── */
.step{ background:#fff; border:1px solid var(--line); border-radius:16px; overflow:hidden; height:100%; position:relative; display:flex; flex-direction:column; }
.step-n{ position:absolute; top:16px; left:16px; z-index:2; background:var(--navy); color:#fff; font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:14px; padding:6px 12px; border-radius:3px; letter-spacing:0.04em; }
.step-img{ aspect-ratio:16/10; background:var(--bg-2); overflow:hidden; }
.step-img img{ width:100%; height:100%; object-fit:contain; }
.step h4{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:20px; text-transform:uppercase; color:var(--navy); margin:20px 24px 10px; }
.step p{ margin:0 24px 24px; font-size:14px; color:var(--ink-2); }

/* ─── Why-teams-switch / testimonials surface ────────────────────────────── */
.sec-testimonials{ background:var(--bg-2); }
.why{ background:#fff; border:1px solid var(--line); border-radius:18px; overflow:hidden; height:100%; display:flex; flex-direction:column; }
.why.dark{ background:var(--navy); border-color:var(--navy); color:#fff; }
.why-img{ aspect-ratio:16/10; background:var(--bg-2); overflow:hidden; }
.why-img img, .why-img video{ width:100%; height:100%; object-fit:cover; display:block; }
.why.dark .why-img{ background:#000; }
.why-body{ padding:24px 26px 28px; display:flex; flex-direction:column; gap:12px; flex:1; }
.why-body h4{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:20px; text-transform:uppercase; color:var(--navy); margin:0; line-height:1.15; }
.why.dark .why-body h4{ color:#fff; }
.why-body p{ font-size:14px; color:var(--ink-2); margin:0; }
.why.dark .why-body p{ color:rgba(255,255,255,.78); }
.why-body ul{ list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:6px; }
.why-body ul li{ font-size:13px; color:var(--ink-2); display:flex; gap:10px; align-items:flex-start; }
.why-body ul li i{ color:var(--accent); margin-top:4px; font-size:10px; }
.why.dark .why-body ul li{ color:rgba(255,255,255,.78); }
.why.dark .why-body ul li i{ color:var(--accent-tint); }

/* ─── FAQ accordion (rendered directly from tunnelsai/_faq_data.html on
       every page now -- see docs/SEO_STRATEGY.md Phase 6) ───────────────── */
.sec-faq{ background:var(--bg-2); }
.design-b .accordion,
.sec-faq .accordion{ border:1px solid var(--line); border-radius:16px; overflow:hidden; background:#fff; }
.design-b .accordion-item,
.sec-faq .accordion-item{ border:0 !important; border-bottom:1px solid var(--line) !important; border-radius:0 !important; background:#fff; }
.design-b .accordion-item:last-child,
.sec-faq .accordion-item:last-child{ border-bottom:0 !important; }
.design-b .accordion-button,
.sec-faq .accordion-button{ font-family:'Saira Semi Condensed',sans-serif; font-weight:600; font-size:16px; color:var(--navy) !important; background:#fff !important; box-shadow:none !important; padding:20px 26px; text-transform:uppercase; letter-spacing:0.01em; }
.design-b .accordion-button:not(.collapsed),
.sec-faq .accordion-button:not(.collapsed){ color:var(--navy) !important; background:#fff !important; }
.design-b .accordion-button::after,
.sec-faq .accordion-button::after{ background:none; content:"+"; color:var(--accent); font-family:'IBM Plex Mono',monospace; font-size:20px; transition:transform .2s; width:auto; height:auto; }
.design-b .accordion-button:not(.collapsed)::after,
.sec-faq .accordion-button:not(.collapsed)::after{ transform:rotate(45deg); }
.design-b .accordion-button i{ color:var(--accent); }
.design-b .accordion-body,
.sec-faq .accordion-body{ padding:0 26px 22px; font-size:14px; color:var(--ink-2); line-height:1.6; }
.sec-faq .accordion-body p{ margin-bottom:10px; }
.sec-faq .accordion-body p:last-child{ margin-bottom:0; }

/* ─── Final CTA band ─────────────────────────────────────────────────────── */
.sec-final{ background:var(--navy); color:#fff; position:relative; overflow:hidden; }
.sec-final::before{ content:""; position:absolute; inset:0; background:radial-gradient(circle at 80% 20%,rgba(76,86,211,.22),transparent 50%),radial-gradient(circle at 20% 80%,rgba(60,66,182,.18),transparent 55%); }
.final-h{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:64px; line-height:1; color:#fff; text-transform:uppercase; letter-spacing:-0.02em; }
.final-h em{ font-style:normal; color:var(--accent-tint); font-weight:800; }

/* ─── Responsive type defaults (about.html overrides these locally) ─────── */
@media (max-width: 991.98px){
  .display-h1{ font-size:48px; }
  .display-h2{ font-size:38px; }
  .final-h{ font-size:42px; }
  .uc{ padding:24px; }
}

/* ─── Breadcrumbs — tunnelsai/templates/tunnelsai/breadcrumbs.html ───────
   Moved here from blog/static/blog/blog-nav.css (Phase 3, docs/SEO_STRATEGY.md)
   so marketing pages, which don't load that file, can render breadcrumbs
   too (currently /compare/<slug>). Blog keeps getting the rule via
   layout.html — this is a move, not a copy, so there's one source of truth. */
.breadcrumb-nav{ padding:18px 0 0; }
.breadcrumb-nav ol{ display:flex; flex-wrap:wrap; gap:6px; margin:0; padding:0; list-style:none; font-size:13px; color:var(--mute); }
.breadcrumb-nav li{ display:flex; align-items:center; gap:6px; }
.breadcrumb-nav li:not(:last-child)::after{ content:"/"; color:var(--line); margin-left:6px; }
.breadcrumb-nav a{ color:var(--accent); text-decoration:none; }
.breadcrumb-nav a:hover{ text-decoration:underline; }
.breadcrumb-nav [aria-current="page"]{ color:var(--ink-2); }

/* ─── Comparison pages (/compare) — docs/SEO_STRATEGY.md Phase 3 ────────── */
.cmp-legend{ font-size:13px; color:var(--mute); margin-bottom:16px; }
.cmp-matrix-card{ background:#fff; border:1px solid var(--line); border-radius:18px; overflow:hidden; }
.cmp-matrix{ margin-bottom:0; }
.cmp-matrix th, .cmp-matrix td{ padding:16px 18px; font-size:14px; }
.cmp-matrix thead th{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; text-transform:uppercase; font-size:12px; letter-spacing:0.03em; color:var(--navy); background:var(--bg); border-bottom:1px solid var(--line); }
.cmp-matrix tbody th{ font-weight:600; color:var(--navy); }
.cmp-matrix tbody tr:not(:last-child) th, .cmp-matrix tbody tr:not(:last-child) td{ border-bottom:1px solid var(--line); }
.cmp-source{ display:inline-block; margin-left:6px; font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.03em; text-transform:uppercase; color:var(--accent); text-decoration:underline; }
.cmp-matrix thead th:nth-child(2){ background:rgba(27,95,168,.06); color:var(--accent); }
.cmp-matrix tbody td:nth-child(2){ background:rgba(27,95,168,.035); }

@media (max-width: 767.98px){
  .cmp-matrix{ min-width:620px; }
  .cmp-matrix thead th:first-child, .cmp-matrix tbody th{ position:sticky; left:0; z-index:1; min-width:170px; }
  .cmp-matrix tbody th{ background:#fff; }
  .cmp-matrix thead th:first-child{ background:var(--bg); }
}

.cmp-pick-card, .cmp-stance{ background:#fff; border:1px solid var(--line); border-radius:18px; padding:28px 30px; height:100%; }
.cmp-pick-card h3{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:19px; text-transform:uppercase; color:var(--navy); letter-spacing:-0.005em; margin-bottom:16px; }
.cmp-pick-card ul{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:10px; }
.cmp-pick-card li{ display:flex; align-items:flex-start; gap:10px; font-size:14px; color:var(--ink-2); line-height:1.5; }
.cmp-pick-card li i{ font-size:5px; margin-top:7px; color:var(--accent); flex:0 0 auto; }
.cmp-pick-card--them{ background:var(--bg-2); }

.cmp-verdict{ background:#fff; border:1px solid var(--line); border-left:3px solid var(--accent); border-radius:18px; padding:32px 36px; }
.cmp-verdict-lbl{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.14em; text-transform:uppercase; color:var(--accent); margin-bottom:12px; }
.cmp-verdict p{ font-size:17px; line-height:1.65; color:var(--ink-2); margin:0; max-width:78ch; }

.cmp-stance{ border-top:3px solid var(--accent); }
.cmp-stance--them{ background:var(--bg-2); border-top-color:var(--line); }
.cmp-stance-lbl{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase; color:var(--mute); margin-bottom:8px; }
.cmp-stance h3{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:24px; text-transform:uppercase; color:var(--navy); letter-spacing:-0.005em; margin-bottom:14px; }
.cmp-stance p{ font-size:15px; line-height:1.6; color:var(--ink-2); margin:0; max-width:60ch; }

@media (max-width: 991.98px){ .cmp-verdict p{ font-size:16px; } }

.cmp-card{ display:flex; flex-direction:column; height:100%; background:#fff; border:1px solid var(--line); border-radius:18px; padding:26px; text-decoration:none; color:inherit; transition:border-color .2s, transform .2s, box-shadow .2s; }
.cmp-card:hover{ border-color:var(--navy); transform:translateY(-2px); box-shadow:0 18px 40px -25px rgba(0,0,0,.15); }
.cmp-card-kicker{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase; color:var(--mute); margin-bottom:6px; }
.cmp-card h3{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:22px; text-transform:uppercase; color:var(--navy); letter-spacing:-0.005em; margin-bottom:10px; }
.cmp-card p{ font-size:14px; color:var(--ink-2); line-height:1.5; flex:1; }
.cmp-card-cta{ font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:0.03em; text-transform:uppercase; color:var(--accent); margin-top:12px; }

/* Sources & method note -- provenance + non-affiliation notice. Docked to the
   bottom of .cmp-matrix-card (_compare_matrix.html) so it reads as a table
   source-note and the "checked" date sits beside the claims it qualifies. The
   base (dashed rule, no fill) is used undocked on compare_index.html. Replaced
   .cmp-disclaimer, a centered <p> orphaned after the final CTA band. */
.cmp-note{ padding-top:18px; border-top:1px dashed var(--line); }
.cmp-note-lbl{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.14em; text-transform:uppercase; color:var(--mute); margin-bottom:8px; }
.cmp-note p{ font-size:12.5px; line-height:1.6; color:var(--ink-2); margin:0; max-width:92ch; }
.cmp-note a{ color:var(--accent); text-decoration:underline; }
.cmp-note--docked{ padding:20px 18px 22px; border-top:1px solid var(--line); background:var(--bg); }

/* ---------- Cookie consent banner (Consent Mode v2) ----------
   tunnelsai/templates/tunnelsai/cookie_banner.html, included site-wide by
   layout.html. z-index 1035: above the footer (95), the glossary sticky
   jumpnav (10), and Bootstrap 5.3's dropdown/sticky/fixed (1000/1020/1030),
   but below its offcanvas-backdrop/modal-backdrop/modal (1040/1050/1055) so
   #registerModal and the pricing modal still render on top of it. Pure
   vanilla CSS -- no Bootstrap JS dependency (see cookie_banner.html's own
   comment). Only tokens declared in BOTH :root blocks (this file and
   layout.html's inline copy) are used here.
   Two breakpoints below: <=768px stacks the layout and tightens spacing so
   the fixed bottom bar doesn't eat a large share of a phone viewport;
   <=575.98px additionally drops the heading line (the region's
   aria-label="Cookie consent" already gives sighted desktop users the same
   context, so it's redundant to spend ~25px of a short viewport on it). */
.cc-bar{
  position:fixed; left:0; right:0; bottom:0; z-index:1035;
  background:var(--bg); color:var(--ink);
  border-top:1px solid var(--line);
  box-shadow:0 -4px 24px rgba(0,0,0,.12);
}
.cc-bar[hidden]{ display:none; }
.cc-inner{
  max-width:1140px; margin:0 auto; padding:18px 24px;
  display:flex; align-items:center; gap:24px; flex-wrap:wrap;
}
.cc-text{ flex:1 1 420px; min-width:260px; font-size:14px; line-height:1.5; color:var(--ink-2); }
.cc-text .cc-heading{ font-weight:600; color:var(--navy); }
.cc-text a{ color:var(--accent); text-decoration:underline; }
.cc-actions{ flex:0 0 auto; display:flex; gap:12px; }
.cc-btn{ border:0; border-radius:8px; padding:10px 20px; font-weight:600; font-size:14px; white-space:nowrap; }
.cc-btn--reject{ background:transparent; color:var(--ink-2); border:1px solid var(--line); }
.cc-btn--reject:hover{ border-color:var(--mute); color:var(--navy); }
.cc-btn--accept{ background:var(--accent); color:#fff; }
.cc-btn--accept:hover{ background:var(--accent-2); }
@media (max-width:768px){
  .cc-inner{ flex-direction:column; align-items:stretch; gap:12px; padding:14px 16px; }
  .cc-text{ flex:1 1 auto; min-width:0; font-size:13px; line-height:1.45; }
  .cc-actions{ justify-content:stretch; gap:10px; }
  .cc-btn{ flex:1 1 0; padding:12px 14px; min-height:44px; }
}
@media (max-width:575.98px){
  .cc-text .cc-heading{ display:none; }
}

/* ─── COST DATA HUB (Phase 4, docs/SEO_STRATEGY.md) ───
   Tables and cards reuse .cmp-matrix/.cmp-matrix-card/.cmp-card/.cmp-note
   from the /compare pages -- this block only covers what's genuinely new:
   the sortable-column affordance and the search input on the hub table. */
#cd-hub-table th[data-sort]{ user-select:none; transition:color .15s ease; }
#cd-hub-table th[data-sort]:hover{ color:var(--accent); }
#cd-hub-table th[data-sort] i{ font-size:10px; margin-left:4px; color:var(--mute); }
#cd-search{ border-radius:10px; border-color:var(--line); }
#cd-search:focus{ border-color:var(--accent); box-shadow:0 0 0 3px rgba(27,95,168,.12); }

/* ─── ESTIMATING TOOLS (docs/ESTIMATING_TOOLS_FRAMEWORK.md) ───
   Purpose-built .est-* calculator-card classes, not restyled Bootstrap
   utilities -- that's what forced the !important pile-up in .sec-calc
   (business-case.html), a mistake this block deliberately avoids. Shared
   by every tool_<slug>.js calculator via _estimating_calc.html; only the
   inline SVG content inside #est-viz differs per tool.
   Surface convention: the card itself is a light grey recess (#f8f9fa,
   Bootstrap's bg-light -- deliberately not the --bg/--bg-2 tokens, and one
   step lighter than the page's own var(--bg) behind it); the diagram
   (.est-calc-viz), quantity table wrap (.est-qty-table-wrap), unit/mode
   toggles and upload dropzone are white panels; the actual input fields
   (.est-field input/select, .est-qty-table .est-cell, the grid-size
   stepper) are explicitly set to white too, since Bootstrap's own white no
   longer applies here -- this file redefines --bs-body-bg to var(--bg) at
   :root, so .form-control/.form-select resolve to the page's grey by
   default. Every such input rule needs a matching :focus rule with its own
   background:#fff, because .form-control:focus re-asserts
   background-color:var(--bs-body-bg) at a specificity that otherwise wins.
   .est-output-tile (the result tiles) deliberately stays var(--bg), not
   white -- it's the one thing in the card that keeps its original colour.
   Don't reintroduce a var(--bg) panel background on the diagram/table/
   toggle/upload wrappers; it disappears against the new card colour. */
.est-calc-card{
  background:#f8f9fa; border:1px solid var(--line); border-radius:18px; padding:26px;
  display:flex; flex-direction:column; gap:22px;
}

/* Diagram (left) / calculation controls (right) columns, shared by every tool via
   _estimating_calc.html. minmax(0, ...) on both tracks keeps a wide child (the
   quantity table) from forcing its track wider than intended instead of using the
   table's own overflow-x:auto. Collapses to one column on mobile, stacking in DOM
   order (diagram above calculation) -- no CSS `order` needed either way. */
.est-calc-columns{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.5fr); gap:24px; align-items:start; }
.est-calc-col-viz, .est-calc-col-inputs{ min-width:0; display:flex; flex-direction:column; gap:22px; }
@media (max-width:991.98px){
  .est-calc-columns{ grid-template-columns:1fr; }
}
/* Cut-and-fill tools only (tool.full_width_viz): full-width diagram on top, full-width
   inputs/table below, at every screen width -- their 1000x380 diagrams (area-weighted
   depth chart / plan-view mesh) read cramped at half the card's width. Two classes
   beats the base rule's specificity, no !important needed. */
.est-calc-columns--stacked{ grid-template-columns:1fr; }
.est-calc-inputs{ display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr)); gap:14px; }
.est-field{ display:flex; flex-direction:column; }
.est-field label{
  display:block; font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.04em;
  text-transform:uppercase; color:var(--mute); margin-bottom:6px;
}
.est-field .est-unit{ color:var(--mute); font-weight:400; text-transform:none; letter-spacing:0; }
/* margin-top:auto pins the control to the bottom of its (grid-stretched) .est-field cell, so every
   input in a .est-shared-inputs row shares a common top/bottom edge regardless of how many lines
   its own label wrapped to -- labels vary in length per tool ("Truck capacity (heaped) (m³)" vs
   "Practical fill factor (%)") and used to leave inputs at different heights in the same row. */
.est-field input, .est-field select{ margin-top:auto; border-radius:8px; border-color:var(--line); background:#fff; }
/* :focus needs its own background:#fff -- Bootstrap's .form-control:focus (specificity 0,2,0)
   re-asserts background-color:var(--bs-body-bg), which this stylesheet redefines to var(--bg)
   at :root -- that out-specifies the (0,1,1) rule above, so without this the field would grey
   out the moment you click into it. */
.est-field input:focus, .est-field select:focus{ border-color:var(--accent); box-shadow:0 0 0 3px rgba(27,95,168,.12); background:#fff; }

.est-calc-viz{ position:relative; background:#fff; border:1px solid var(--line); border-radius:14px; padding:14px; }
.est-calc-viz svg{ width:100%; height:auto; display:block; font-family:'IBM Plex Mono',monospace; }

/* Compact/Expanded layout toggle (round 16, moved onto the graphic from
   .est-toolbar) -- overlaid on .est-calc-viz (its `position:relative` above
   is what anchors this) rather than inside .est-viz3d specifically, so it
   sits over whichever layer -- the SVG fallback or the 3D canvas -- is
   actually visible. z-index above .est-viz3d-labels (below, pointer-events:
   none already) so it stays clickable even if a diagram label happens to
   render underneath it. Same .est-unit-toggle/.est-unit-btn pill as every
   other toggle on the card, just noticeably smaller -- a corner control, not
   a toolbar-weight one. */
.est-viz-layout-toggle{ position:absolute; top:10px; right:10px; z-index:2; }
/* Compound selector (both classes) so this padding override beats
   .est-unit-toggle's own padding:3px below on specificity -- same class
   count, but declared earlier in the file, would otherwise lose the
   cascade on source order alone despite being the more specific intent. */
.est-unit-toggle.est-viz-layout-toggle{ padding:2px; }
.est-viz-layout-toggle .est-unit-btn{ padding:4px 10px; font-size:10.5px; }

/* Any viz3d tool (basic-cut-and-fill, advanced-cut-and-fill, concrete-volume,
   formwork-area, slab-screed-volume, aggregate-sub-base, asphalt-tonnage,
   dewatering-volume): three.js scene, layered over the SVG chart above. Hidden until
   tool_<slug>.js confirms three.js loaded AND a WebGL context was obtained
   -- at that point it adds .est-viz3d--active here and .est-viz-hidden-3d on
   the sibling <svg>, so a visitor with WebGL disabled, a failed script load,
   or JS off entirely always keeps the working 2D chart instead of an empty
   box. Not scoped to a single tool's [data-tool] any more -- eight tools
   share this now. */
#est-viz.est-viz-hidden-3d{ display:none; }
.est-viz3d{
  display:none; position:relative; width:100%; aspect-ratio:5/3; border-radius:8px; overflow:hidden;
}
/* Compact (non-expanded) layout only: a little taller than the shared 5/3
   above, so the Compact/Expanded toggle overlaid in the corner (right above)
   has breathing room against the diagram's own labels -- the expanded/
   stacked layout is already wide enough that 5/3 reads comfortably tall. */
.est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d{ aspect-ratio:4/3; }
.est-viz3d.est-viz3d--active{ display:block; }
.est-viz3d canvas{ width:100%; height:100%; display:block; cursor:grab; touch-action:pan-y; }
.est-viz3d canvas:active{ cursor:grabbing; }
.est-viz3d-labels{ position:absolute; inset:0; pointer-events:none; }
.est-viz3d-label{
  position:absolute; left:0; top:0; display:none; white-space:nowrap;
  font-family:'IBM Plex Mono',monospace; font-size:11px; font-weight:600;
  padding:1px 6px; border-radius:4px; background:rgba(255,255,255,.88);
}
.est-viz3d-label--cut{ color:#C0392B; }
.est-viz3d-label--fill{ color:#3E8C5A; }
.est-viz3d-label--datum{ color:#5c6b74; font-weight:500; }
.est-viz3d-label--neutral{ color:#7a766e; font-weight:500; }
/* dewatering-volume's single "Original water table" label -- same blue as the pit rim outline
   and pit water material in that tool's viz3d scene (module-scope WATER_COLOR, 0x2a78d6),
   reserved for water only across that tool's whole palette. */
.est-viz3d-label--water{ color:#2a78d6; font-weight:500; }

.est-calc-outputs{ display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); gap:12px; }
.est-output-tile{ background:var(--bg); border:1px solid var(--line); border-radius:12px; padding:12px 16px; text-align:center; }
.est-output-label{
  font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:.10em; text-transform:uppercase;
  color:var(--mute); margin-bottom:4px; line-height:1.3;
}
.est-output-value{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:28px; color:var(--navy); line-height:1.1; display:inline; }
.est-output-unit{ font-size:13px; color:var(--mute); margin-top:0; margin-left:6px; display:inline; }

/* Basic cut-and-fill only: color each tile to what it's showing, same red/green pairing
   as the diagram (round 5) -- the other tools' outputs have no built-in cut/fill/net
   meaning, so they stay the plain tile above, just resized. */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(1){ background:rgba(192,57,43,.08); border-color:rgba(192,57,43,.28); }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(1) .est-output-value{ color:#C0392B; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(2){ background:rgba(62,140,90,.10); border-color:rgba(62,140,90,.30); }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(2) .est-output-value{ color:#3E8C5A; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3){ background:rgba(62,140,90,.10); border-color:rgba(62,140,90,.30); }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-value{ color:#3E8C5A; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative{ background:rgba(192,57,43,.08); border-color:rgba(192,57,43,.28); }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative .est-output-value{ color:#C0392B; }

/* Net-as-headline layout, basic cut-and-fill only -- the three tiles above are
   equal-weight by default; here Net (the number an estimator actually came
   for) becomes a full-width headline row, with Cut/Fill demoted to a smaller
   supporting row beneath. order:-1 on the netVol tile (3rd in DOM order)
   moves it to that headline row without touching _estimating_calc.html's
   shared, tool-agnostic markup. The direction cue (arrow + trailing label
   text) reuses .est-output-tile--negative, which est-common.js already
   toggles for netVol < 0 -- no JS changes needed. */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-calc-outputs{ grid-template-columns:1fr 1fr; }
@media (max-width:575.98px){
  #est-calc-card[data-tool="basic-cut-and-fill"] .est-calc-outputs{ grid-template-columns:1fr; }
}
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3){
  grid-column:1 / -1; order:-1; text-align:left; padding:20px 24px;
}
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-value{ font-size:40px; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-unit{ font-size:15px; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(1),
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(2){ padding:10px 14px; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(1) .est-output-value,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(2) .est-output-value{ font-size:20px; }
/* ▲ = net positive = fill > cut = material needed on site; ▼ = net negative =
   cut > fill = surplus to haul away -- matches aggregate()'s sign convention
   in tool_basic-cut-and-fill.js and the existing red/green flip above. */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-value::before{ content:"▲ "; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative .est-output-value::before{ content:"▼ "; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-label::after{ content:" — material to import"; }
#est-calc-card[data-tool="basic-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative .est-output-label::after{ content:" — surplus to remove"; }

/* Advanced grid tool -- identical tile coloring to the basic tool above, just scoped
   to its own data-tool value. */
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(1){ background:rgba(192,57,43,.08); border-color:rgba(192,57,43,.28); }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(1) .est-output-value{ color:#C0392B; }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(2){ background:rgba(62,140,90,.10); border-color:rgba(62,140,90,.30); }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(2) .est-output-value{ color:#3E8C5A; }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(3){ background:rgba(62,140,90,.10); border-color:rgba(62,140,90,.30); }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(3) .est-output-value{ color:#3E8C5A; }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative{ background:rgba(192,57,43,.08); border-color:rgba(192,57,43,.28); }
#est-calc-card[data-tool="advanced-cut-and-fill"] .est-output-tile:nth-child(3).est-output-tile--negative .est-output-value{ color:#C0392B; }

.est-calc-actions{ display:flex; justify-content:flex-end; gap:12px; }
.est-calc-actions .btn-outline-navy{ font-size:12px; padding:10px 18px; }

/* ─── ESTIMATING TOOLS: grid-input widget (docs/ESTIMATING_TOOLS_FRAMEWORK.md redesign
   plan, round 8) -- generic, not scoped to any one tool's data-tool value, so a future
   grid_input tool can reuse it as-is. The Existing/Proposed layer toggle deliberately
   reuses .est-unit-toggle/.est-unit-btn verbatim (see _estimating_calc_grid.html) rather
   than inventing a second visual pattern for what's functionally the same kind of switch. */
.est-grid-toolbar{ display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:14px; }
.est-grid-size{ display:flex; align-items:center; gap:8px; font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:.03em; color:var(--mute); }
.est-grid-size label{ text-transform:uppercase; letter-spacing:.06em; font-size:10.5px; }
.est-grid-size input[type="number"]{
  width:44px; text-align:center; border:1px solid var(--line); border-radius:6px; padding:5px 2px;
  font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--navy); background:#fff;
}
.est-grid-size input[type="number"]:focus{ border-color:var(--accent); box-shadow:0 0 0 3px rgba(27,95,168,.12); outline:none; }
.est-grid-size button{
  width:26px; height:26px; border:1px solid var(--line); border-radius:6px; background:#fff;
  color:var(--navy); font-size:14px; line-height:1; cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
}
.est-grid-size button:hover{ background:var(--bg); }

.est-grid-wrap{ overflow-x:auto; }
.est-grid-table{ border-collapse:collapse; margin:0 auto; }
.est-grid-table td{ padding:3px; }
.est-grid-cell{
  width:64px; height:44px; text-align:center; border:1px solid var(--line); border-radius:6px;
  font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--navy); background:#fff;
}
.est-grid-cell:focus{ border-color:var(--accent); box-shadow:0 0 0 3px rgba(27,95,168,.12); outline:none; }
.est-grid-cell.est-cell-invalid{ border-color:#a6320b; background:rgba(166,50,11,.06); }

/* ─── ESTIMATING TOOLS: units + quantity table + upload
   (docs/ESTIMATING_TOOLS_FRAMEWORK.md §10-11) ───
   Extends the .est-* block above -- same "purpose-built classes, not
   restyled Bootstrap utilities" discipline. */
.est-toolbar{
  display:flex; justify-content:flex-end; align-items:center; gap:14px;
  padding-bottom:16px;
}
.est-unit-toggle{ display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:3px; background:#fff; }
.est-unit-btn{
  border:none; background:none; padding:6px 16px; border-radius:999px; font-size:12px;
  font-family:'IBM Plex Mono',monospace; letter-spacing:.03em; color:var(--mute); cursor:pointer;
}
.est-unit-btn.selected{ background:var(--navy); color:#fff; }

/* Basic/Advanced mode toggle (_estimating_mode_toggle.html) -- reuses .est-unit-toggle/
   .est-unit-btn verbatim (same precedent as the grid tool's Existing/Proposed toggle,
   _estimating_calc_grid.html) so no new pill visual is needed. Sits on the same (right) side as
   the Metric/Imperial toggle via .est-toolbar's own justify-content:flex-end + gap -- no extra
   positioning needed here. Tools with no variant_slug (this partial renders nothing) are
   unaffected either way. */
.est-mode-toggle .selected{ cursor:default; }
/* Loading state for the *other* mode's link -- clicking it is a real page
   navigation (a plain <a href>, see _estimating_mode_toggle.html), not an
   AJAX swap, so this only needs to cover the visible gap while the new
   page's request is in flight (see the inline script in estimating_tool.html
   that swaps the link's content for the spinner and defers navigation by two
   animation frames so the browser actually paints it first). */
.est-mode-toggle a.est-unit-btn.est-mode-toggle-loading{
  pointer-events:none; opacity:.75; display:inline-flex; align-items:center; justify-content:center;
}
.est-mode-toggle .spinner-border{ width:0.8rem; height:0.8rem; border-width:2px; }

.est-shared-inputs{ display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr)); gap:14px; }

.est-upload{ display:none; border:1px dashed var(--line); border-radius:14px; padding:18px; background:#fff; }
.est-upload.is-open{ display:block; }
.est-dropzone{ cursor:pointer; text-align:center; padding:10px 6px; border-radius:10px; }
.est-dropzone:focus-visible, .est-dropzone--over{ outline:2px solid var(--accent); outline-offset:2px; background:rgba(27,95,168,.06); }
.est-dropzone-headline{ margin-bottom:4px; }
.est-upload-link{ color:var(--accent); text-decoration:underline; cursor:pointer; }
.est-upload-hint, .est-upload-privacy{ font-size:12px; color:var(--mute); margin:2px 0 0; }
.est-upload-actions{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; margin-top:14px; }
.est-upload-actions .btn-outline-navy{ font-size:12px; padding:8px 14px; }
.est-upload-notice{ margin-top:12px; font-size:12px; color:var(--ink-2); background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px 12px; }
.est-upload-notice--error{ color:#a6320b; border-color:rgba(166,50,11,.35); background:rgba(166,50,11,.06); }

.est-qty-table-wrap{ overflow-x:auto; border:1px solid var(--line); border-radius:14px; background:#fff; }
.est-qty-table{ width:100%; border-collapse:collapse; font-size:13px; }
.est-qty-table thead th{
  font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:.03em; text-transform:uppercase;
  color:var(--mute); font-weight:600; text-align:left; padding:10px 10px; background:var(--bg); white-space:nowrap;
  border-bottom:1px solid var(--line);
}
.est-qty-table tbody td{ padding:7px 8px; border-bottom:1px solid var(--line); vertical-align:middle; }
.est-qty-table tbody tr:last-child td{ border-bottom:none; }
.est-qty-table tbody tr:nth-child(even) td{ background:var(--bg); }
.est-qty-table .est-cell{ min-width:88px; border-radius:6px; border-color:var(--line); background:#fff; }
/* :focus needs its own background:#fff for the same reason as .est-field input:focus above --
   .form-control:focus (0,2,0) ties with the base rule here and would otherwise win on source
   order, reverting to var(--bs-body-bg)/var(--bg) while a cell is being edited. */
.est-qty-table .est-cell:focus{ background:#fff; }
.est-qty-table td:first-child .est-cell{ min-width:140px; border-color:transparent; background:transparent; }
.est-qty-table td:first-child .est-cell:hover{ border-color:var(--line); }
.est-qty-table td:first-child .est-cell:focus{ border-color:var(--accent); background:#fff; box-shadow:0 0 0 3px rgba(27,95,168,.12); }
.est-qty-table .est-cell.est-cell-invalid{ border-color:#a6320b; background:rgba(166,50,11,.06); }
.est-qty-table .est-row-output{ font-family:'Saira Semi Condensed',sans-serif; font-weight:600; font-size:17px; color:var(--navy); white-space:nowrap; }
.est-qty-table .est-row-output--zero{ font-weight:400; color:var(--mute); opacity:.6; }

/* Per-row color accent (concrete-volume, formwork-area only) -- a 4px
   left-edge strip on each row's name cell, cycling an 8-hue categorical
   palette by row POSITION (nth-of-type), not by row identity: row 1 always
   gets slot 1 regardless of which named row currently sits there, since
   that's the same index each tool's viz3d IIFE and drawSvgFallback use to
   color that row's own box/panel/SVG bar -- no per-row id tracking needed
   across add/delete, position is already the shared key. Palette is the
   dataviz skill's validated default categorical set (references/palette.md),
   confirmed via its validator against this table's actual white surface --
   not hand-picked. Text itself stays in the existing dark ink tokens (never
   the accent color) per that skill's own rule and because 3 of the 8 hues
   (aqua/yellow/magenta) read below 3:1 on white -- fine for a border strip
   or a solid-lit 3D material, not for text. Base rule reserves the 4px even
   before a row exists, so there's no reflow when JS clones a new row. */
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody td:first-child{
  border-left:4px solid transparent;
}
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+1) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+1) td:first-child{ border-left-color:#2a78d6; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+2) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+2) td:first-child{ border-left-color:#eb6834; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+3) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+3) td:first-child{ border-left-color:#1baf7a; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+4) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+4) td:first-child{ border-left-color:#eda100; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+5) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+5) td:first-child{ border-left-color:#e87ba4; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+6) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+6) td:first-child{ border-left-color:#008300; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n+7) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n+7) td:first-child{ border-left-color:#4a3aa7; }
#est-calc-card[data-tool="concrete-volume"] .est-qty-table tbody tr:nth-of-type(8n) td:first-child,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody tr:nth-of-type(8n) td:first-child{ border-left-color:#e34948; }

/* formwork-area column-width redistribution (2026-09-01) -- not part of the
   font-shrink/header-wrap group below (its 4-column table never needed
   that), so no header max-width change -- Length/Height's own headers are
   short single words and stay on the shared nowrap sizing. Column order:
   1 Element, 2 Length, 3 Height, 4 Faces formed (select), 5 Area. Element
   and Faces formed ("One side"/"Two sides") are the expand targets. */
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody td:nth-child(2) .est-cell,
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody td:nth-child(3) .est-cell{
  min-width:56px;
}
#est-calc-card[data-tool="formwork-area"] .est-qty-table td:first-child .est-cell{
  min-width:170px;
}
#est-calc-card[data-tool="formwork-area"] .est-qty-table tbody td:nth-child(4) .est-cell{
  min-width:130px;
}

/* aggregate-sub-base / asphalt-tonnage (2026-08-31) -- diagram made a little
   smaller so the quantity table reads bigger, and the on-figure per-tile
   labels (removed from both tool_<slug>.js's viz3d rebuild()) are replaced
   by a colour cue on each row keyed to computeRow's `swatch` field via the
   generic --est-row-swatch custom property (est-common.js). Scoped to these
   two slugs only -- the base .est-calc-columns ratio and .est-viz3d aspect
   ratio are shared by every other tool and stay untouched.
   rebar-weight (2026-08-31, later) joins the same three selector groups below
   for the same reason -- its bar-bundle scene also keys row identity to a
   colour (bar size, not material) via the same swatch/--est-row-swatch hook,
   and also renders no per-tile overlay labels or Compact/Expanded toggle.
   dewatering-volume (2026-08-31, later still) joins all four selector groups
   too (moved off the shared default ratio per explicit request) -- same
   reasoning: expandable_viz=False, no overlay labels, and its own colour
   cue via the same swatch/--est-row-swatch hook.
   soakaway-suds-sizing (2026-09-01) joins the first three groups for the same
   reasoning -- expandable_viz=False, no overlay labels (catchment surface
   type and required-storage labels are sparse and non-per-row), and its own
   colour cue via the same swatch/--est-row-swatch hook (surface type). It was
   already in the fourth (denser-table) group before gaining viz3d. */
/* min-width guard matches the base collapse breakpoint (@media max-width:991.98px above) --
   without it this rule's specificity ((1,2,0), from the #est-calc-card[data-tool] id+attribute)
   beats that mobile media query's (0,1,0) outright regardless of viewport, permanently wedging
   both columns side-by-side into a squeeze at phone width instead of letting them collapse to
   1fr. Confirmed live: this broke mobile before the guard was added. */
@media (min-width:992px){
  #est-calc-card[data-tool="aggregate-sub-base"] .est-calc-columns,
  #est-calc-card[data-tool="asphalt-tonnage"] .est-calc-columns,
  #est-calc-card[data-tool="rebar-weight"] .est-calc-columns,
  #est-calc-card[data-tool="dewatering-volume"] .est-calc-columns,
  #est-calc-card[data-tool="steel-metal-weight"] .est-calc-columns,
  #est-calc-card[data-tool="soakaway-suds-sizing"] .est-calc-columns,
  #est-calc-card[data-tool="soil-bulking-swell-factor"] .est-calc-columns{
    grid-template-columns:minmax(0,1fr) minmax(0,1.9fr);
  }
}
/* Undoes the shared compact-layout 4/3 override (above) -- that override
   exists to give the overlaid Compact/Expanded toggle breathing room against
   the diagram's own labels, and none of these tools render that toggle
   (expandable_viz=False) or have overlay labels either. Back to the base
   5/3, which reads shorter at the narrower column width these tools use. */
#est-calc-card[data-tool="aggregate-sub-base"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="asphalt-tonnage"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="rebar-weight"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="dewatering-volume"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="steel-metal-weight"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="soil-bulking-swell-factor"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-calc-columns:not(.est-calc-columns--stacked) .est-viz3d{
  aspect-ratio:5/3;
}
/* 2026-09-01: restyled from an inset 12px square chip (position:relative +
   ::before) to the same border-left "sliver" technique concrete-volume/
   formwork-area use above (styles-v69.css ~1819) -- same visual language
   site-wide for "this row has a colour meaning" now, just reading a
   value-driven custom property (bar size / material / layer type via
   --est-row-swatch) instead of concrete-volume's row-POSITION nth-of-type
   cycle. Deliberately NOT switched to nth-of-type: unlike concrete-volume's
   rows (no inherent category), these four tools' colour carries real row
   identity -- two rows of the same bar diameter/material/layer must stay the
   same colour regardless of position, which only the swatch var preserves.
   slab-screed-volume (2026-09-01, later) joins this selector list too, but
   for the opposite reason concrete-volume was excluded: a floor area has no
   inherent category either, so its colour IS keyed by row position -- but
   via computeRow's new valid-rows-only `index` argument (est-common.js),
   returned as `swatch`, not a CSS nth-of-type cycle. nth-of-type counts
   every <tr> including invalid ones and would drift out of sync with the 3D
   scene's own valid-rows-only tile order the moment one row goes invalid;
   routing it through the same swatch var as the category-keyed tools above
   avoids that regardless of how the colour was chosen. */
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="dewatering-volume"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="slab-screed-volume"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table tbody td:first-child,
#est-calc-card[data-tool="soil-bulking-swell-factor"] .est-qty-table tbody td:first-child{
  border-left:4px solid var(--est-row-swatch, transparent);
}

/* Denser table + two-line headers (2026-09-01) -- these 7 tools have 5-6
   row_columns/row_outputs and/or long shared labels ("Existing ground
   level", "Concrete volume needed") that pushed .est-qty-table past the
   card width at the shared 13px/10.5px sizing, forcing
   .est-qty-table-wrap's overflow-x:auto (base rule, ~1783) so a visitor had
   to scroll sideways to see every input. Scoped to just these 7 slugs --
   every other tool's table (fewer/shorter columns already) is untouched. */
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table,
#est-calc-card[data-tool="trench-volume"] .est-qty-table,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table{
  font-size:12px;
}
/* white-space:normal + max-width is what actually forces a header to wrap
   under table-layout:auto -- a short single-word header ("Area", "Depth")
   still renders on one line inside that box; a long one ("Existing ground
   level") wraps onto two. The unit span (_estimating_calc.html's
   .est-unit-label, text set by est-common.js's applyUnitLabels()) is forced
   onto its own line below the label -- that's the actual "two lines" this
   is for. :empty guard needed because a dimensionless column (Bar mark,
   Zone) still renders the (now block-level) empty span otherwise, leaving a
   blank second line. */
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table thead th,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th,
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table thead th,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table thead th,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table thead th{
  font-size:9.5px; white-space:normal; max-width:96px; padding:8px 6px; line-height:1.25;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table thead th .est-unit-label,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table thead th .est-unit-label{
  display:block; margin-top:1px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table thead th .est-unit-label:empty,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table thead th .est-unit-label:empty{
  display:none;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table tbody td,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table tbody td,
#est-calc-card[data-tool="trench-volume"] .est-qty-table tbody td,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table tbody td,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table tbody td,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table tbody td,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table tbody td{
  padding:6px 6px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table .est-cell,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table .est-cell,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table .est-cell,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table .est-cell,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table .est-cell,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table .est-cell{
  min-width:76px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table td:first-child .est-cell{
  min-width:118px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="trench-volume"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="concrete-mix-ratio"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="soakaway-suds-sizing"] .est-qty-table .est-row-output,
#est-calc-card[data-tool="asphalt-tonnage"] .est-qty-table .est-row-output{
  font-size:15px;
}

/* Column-width redistribution (2026-09-01) -- on these 4 tables, a handful of
   numeric/select columns only ever hold a few digits or a short option
   ("16 mm", "Type 1 MOT...") while the name column (and, on
   aggregate-sub-base, Material too) holds a real human-typed label. Squeeze
   the former, expand the latter -- same technique the shared td:first-child
   rule above already uses (raising .est-cell's min-width), just per-column
   via nth-child since "2nd column" means a different field per tool. Column
   order (row_columns then row_outputs then the fixed action column, per
   estimating_tools.py / _estimating_calc.html's <thead>):
     aggregate-sub-base: 1 Zone, 2 Area, 3 Compacted depth, 4 Material, 5 Volume, 6 Tonnage
     basic-cut-and-fill: 1 Zone, 2 Area, 3 Existing ground level, 4 Proposed level, 5 Cut/Fill
     trench-volume:      1 Run, 2 Length, 3 Base width, 4 Depth, 5 Face area, 6 Volume
     rebar-weight:        1 Bar mark, 2 Cut length, 3 Diameter (select), 4 Quantity, 5 Weight
   Header max-width also drops (from the shared 96px above) on the squeezed
   columns only -- every squeezed label's longest single word ("EXISTING",
   "COMPACTED", "PROPOSED", "DIAMETER", "QUANTITY") is comfortably under 68px
   at this font, so none needs word-breaking to fit; Material/Zone/Bar mark/
   Run's own headers are short enough to stay on the shared 96px unchanged. */
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table thead th:nth-child(2),
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table thead th:nth-child(3),
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th:nth-child(2),
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th:nth-child(3),
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table thead th:nth-child(4),
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th:nth-child(2),
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th:nth-child(3),
#est-calc-card[data-tool="trench-volume"] .est-qty-table thead th:nth-child(4),
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th:nth-child(2),
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th:nth-child(3),
#est-calc-card[data-tool="rebar-weight"] .est-qty-table thead th:nth-child(4){
  max-width:68px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table tbody td:nth-child(2) .est-cell,
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table tbody td:nth-child(3) .est-cell,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table tbody td:nth-child(2) .est-cell,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table tbody td:nth-child(3) .est-cell,
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table tbody td:nth-child(4) .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table tbody td:nth-child(2) .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table tbody td:nth-child(3) .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table tbody td:nth-child(4) .est-cell,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table tbody td:nth-child(2) .est-cell,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table tbody td:nth-child(4) .est-cell{
  min-width:54px;
}
/* Diameter (rebar-weight, 3rd column) is a select showing "16 mm"-style
   text -- a touch more room than a plain number column. */
#est-calc-card[data-tool="rebar-weight"] .est-qty-table tbody td:nth-child(3) .est-cell{
  min-width:60px;
}
/* Expanded name column -- overrides the shared 118px 4 rules above (same
   selector, later in the cascade wins). basic-cut-and-fill/trench-volume/
   rebar-weight squeeze 3 columns into this one column alone, so it gets the
   most; aggregate-sub-base squeezes only 2 and splits the gain with
   Material below, so a smaller bump. */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="trench-volume"] .est-qty-table td:first-child .est-cell,
#est-calc-card[data-tool="rebar-weight"] .est-qty-table td:first-child .est-cell{
  min-width:190px;
}
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table td:first-child .est-cell{
  min-width:150px;
}
/* Material (aggregate-sub-base, 4th column) -- the other expand target,
   a select showing text like "Type 1 MOT (2,150 kg/m³)". */
#est-calc-card[data-tool="aggregate-sub-base"] .est-qty-table tbody td:nth-child(4) .est-cell{
  min-width:150px;
}

/* steel-metal-weight header wrap + column-width redistribution (2026-09-01) --
   "Diameter / width" and "Wall / plate thickness" are long headers over
   plain-number columns that only ever hold a couple of digits, while
   Item / mark holds a real human-typed label. Wrap headers onto two lines
   (same technique as the denser-table group above) and squeeze those two
   columns' width, giving the gain to Item / mark. Column order: 1 Item/mark,
   2 Length, 3 Profile (select), 4 Diameter/width, 5 Wall/plate thickness,
   6 Quantity, 7 Weight. */
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table thead th{
  font-size:9.5px; white-space:normal; max-width:96px; padding:8px 6px; line-height:1.25;
}
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table thead th .est-unit-label{
  display:block; margin-top:1px;
}
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table thead th .est-unit-label:empty{
  display:none;
}
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table thead th:nth-child(4),
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table thead th:nth-child(5){
  max-width:68px;
}
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table tbody td:nth-child(4) .est-cell,
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table tbody td:nth-child(5) .est-cell{
  min-width:56px;
}
#est-calc-card[data-tool="steel-metal-weight"] .est-qty-table td:first-child .est-cell{
  min-width:170px;
}

/* Combined signed Cut/Fill table cell (round 14) -- basic cut-and-fill tool
   only, scoped via the existing #est-calc-card[data-tool] attribute so the
   other tools are unaffected. Replaces the old separate [data-out="cut"]/
   [data-out="fill"] pair (one of which was always zero) with a single
   [data-out="net"] column: green by default (fill/positive), red via the
   generic .est-row-output--negative sign hook (est-common.js, cut/negative),
   gray via the existing --zero hook (neither). The "+" prefix is added only
   on the positive/non-zero case -- fmtDim's Intl.NumberFormat already
   renders a negative value's own "-", so the negative case needs no prefix.
   display:inline-block/a tighter padding used to be set here directly on the
   <td> itself (.est-row-output IS the <td>, see _estimating_calc.html) --
   that breaks a table cell's default vertical-align:middle and shrinks it
   below its row's height, which is why these cells looked top-left-aligned
   and shorter than the input cells next to them. Dropping both lets the cell
   stay a normal table-cell, inheriting .est-qty-table tbody td's own
   vertical-align:middle and padding:7px 8px so every cell in the row matches.
   --zero also gets a bare [data-out] added so its specificity matches
   [data-out="net"] exactly -- without it, a zeroed cell matched both rules
   and the colored one (declared first here, but higher specificity due to
   its extra attribute selector) always won, so a zero value stayed green
   (just faded by --zero's opacity) instead of the plain gray --zero intends.
   (advanced-cut-and-fill never had an equivalent block worth keeping here --
   its grid shell renders no .est-row-output cells at all.) */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-row-output[data-out="net"]{
  background:rgba(62,140,90,.12); color:#3E8C5A; border-radius:6px;
}
#est-calc-card[data-tool="basic-cut-and-fill"] .est-row-output[data-out="net"]:not(.est-row-output--zero)::before{
  content:"+";
}
#est-calc-card[data-tool="basic-cut-and-fill"] .est-row-output--negative[data-out="net"]{
  background:rgba(192,57,43,.12); color:#C0392B;
}
/* :not(.est-row-output--zero) repeated here (redundant in practice -- a
   negative value is never also zero) purely to match the "+" rule above's
   specificity ((0,3,1) each): without it this selector is only (0,2,1) and
   the "+" rule -- despite being the "default" case -- would win the cascade
   on specificity alone regardless of source order, incorrectly prefixing
   a "+" onto negative (cut) values too. */
#est-calc-card[data-tool="basic-cut-and-fill"] .est-row-output--negative[data-out="net"]:not(.est-row-output--zero)::before{
  content:none;
}
#est-calc-card[data-tool="basic-cut-and-fill"] .est-row-output--zero[data-out]{
  background:transparent; color:var(--mute); font-weight:400; opacity:.55;
}
.est-row-actions-col{ width:36px; text-align:center; }
.est-row-delete{
  border:none; background:none; color:var(--mute); font-size:18px; line-height:1; cursor:pointer; padding:2px 8px;
  border-radius:6px;
}
.est-row-delete:hover{ color:#a6320b; background:rgba(166,50,11,.08); }
.est-qty-row--invalid{ background:rgba(166,50,11,.04); }

.est-qty-table-actions{ display:flex; justify-content:flex-start; }
.est-qty-table-actions .btn-outline-navy{ font-size:12px; padding:8px 16px; }

/* Output-tile summary, sits right under the toolbar above the diagram/table. Not sticky --
   scrolls away normally with the rest of the card. No background of its own -- it's a full-bleed
   strip (margin:0 -26px) over the card, so an opaque fill here would paint a band across the card's
   own colour; the .est-output-tile children keep their own var(--bg) background regardless. */
.est-sticky-summary{
  padding:14px 0;
  margin:0 -26px; padding-left:26px; padding-right:26px;
}

/* Cut/fill diagram legend + caption -- hidden on every tool (round 15).
   basic-cut-and-fill dropped its title+legend first (round 11) as redundant
   with the diagram's own "Existing ground"/"Proposed level" labels and the
   result tiles above; advanced-cut-and-fill kept its own "Grid mesh (3D
   terrain)" title + Cut/Fill swatch legend for several rounds as a
   deliberate asymmetry, until the user asked for it to be dropped too
   (round 15) -- there is no longer a tool that shows this. The
   {% if tool.has_row_kind %} template guard keeps the (now permanently
   hidden) markup out of neither tool's DOM meaningfully -- it just never
   displays. `Tool.viz_title`/`viz_note` stay declared on both tools' dataclass
   entries regardless (inert once hidden, same as basic's already were). */
.est-viz-legend{
  display:none; align-items:baseline; gap:14px; flex-wrap:wrap; margin-bottom:10px;
  font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:.10em; text-transform:uppercase; color:var(--mute);
}
.est-viz-legend-swatches{ margin-left:auto; display:flex; gap:14px; font-size:11.5px; text-transform:none; letter-spacing:0; }
.est-viz-legend-swatch{ display:inline-flex; align-items:center; gap:6px; }
.est-viz-legend-dot{ width:12px; height:12px; border-radius:2px; display:inline-block; }
.est-viz-legend-dot--cut{ background:rgba(192,57,43,.30); border:1px solid #C0392B; }
.est-viz-legend-dot--fill{ background:rgba(62,140,90,.22); border:1px dashed #3E8C5A; }

/* Generic above-diagram legend (Tool.viz_legend, 2026-09-01) -- distinct from .est-viz-legend
   above, which is Cut/Fill-specific and permanently display:none. This one renders whenever a
   tool declares it (slab-screed-volume today, the first). Dots are hue-neutral (ink tokens, not a
   palette colour) on purpose: on slab-screed-volume, hue already carries row identity (matched to
   each row's table sliver), so this legend must distinguish its two layers by FILL alone
   (solid vs translucent), never by colour, or it would misleadingly suggest a fixed slab/screed hue. */
.est-viz-key{
  display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:10px;
  font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:.10em; text-transform:uppercase; color:var(--mute);
}
.est-viz-key-item{ display:inline-flex; align-items:center; gap:6px; }
.est-viz-key-dot{ width:12px; height:12px; border-radius:2px; display:inline-block; border:1px solid var(--ink-2); }
.est-viz-key-dot--solid{ background:var(--ink-2); }
.est-viz-key-dot--translucent{ background:rgba(26,26,26,.18); }

.est-viz-note{ font-size:12px; color:var(--mute); margin:12px 0 0; line-height:1.5; }
/* Dynamic below-the-graphic caption (concrete-volume's "Depth exaggerated" note today) --
   present on every viz3d tool's markup but starts empty; collapses to nothing rather than
   leaving a stray blank-line gap on tools that never write into it. */
.est-viz-note--dynamic:empty{ display:none; margin:0; }

/* ─── Estimating-tool page shell: dark hero, theory/FAQ card + sidebar ───
   (docs/ESTIMATING_TOOLS_FRAMEWORK.md redesign plan). Reuses
   .feature-hero--dark's existing full-bleed ::before + isolation:isolate
   technique verbatim (see CLAUDE.md "Feature-page Hero Backgrounds") --
   .est-hero only adds the grid texture and layout on top of it. */
/* py-5 deliberately NOT used on this section -- Bootstrap's utility classes are
   !important (confirmed: .py-5{padding-top:3rem!important;padding-bottom:3rem!important}),
   which silently defeated an earlier padding-bottom override here and let the
   calculator card (negative-margin-overlapped below) intrude into the hero's own
   button/pills. .est-hero now owns its vertical spacing outright. margin-top is the
   breadcrumb breathing-room gap -- margin, not padding, so it sits outside the box
   the full-bleed ::before pseudo covers and doesn't affect that technique. */
.est-hero{ isolation:isolate; padding-top:48px; padding-bottom:96px; }
.est-hero::after{
  content:""; position:absolute; inset:0; pointer-events:none; z-index:0;
  background-image:
    linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px);
  background-size:44px 44px;
  -webkit-mask-image:radial-gradient(820px 440px at 22% 8%, #000, transparent 85%);
  mask-image:radial-gradient(820px 440px at 22% 8%, #000, transparent 85%);
}
.est-hero-top{ position:relative; z-index:1; display:flex; align-items:flex-end; justify-content:space-between; gap:32px; flex-wrap:wrap; }
.est-hero-content{ width:100%; }
.est-hero-content .lede{ max-width:none; }
.est-hero-badge{
  width:44px; height:44px; border-radius:12px; background:rgba(117,135,243,.22);
  border:1px solid rgba(146,164,252,.34); display:inline-flex; align-items:center; justify-content:center; flex:0 0 auto;
}
/* Every tabler-*.svg icon (tunnelsai/static/tunnelsai/images/lightmode_icons/estimating/) is a
   uniform fill:none / stroke:#1a1a1a graphic rendered via a plain <img>, so CSS can't reach its
   stroke directly -- brightness(0) invert(1) is the standard forced-white recolor for a single-
   color <img>-embedded SVG. Scoped to this badge only: the same icon files are also used by
   estimating_hub.html's .dt-tile-icon on a light tile background, which must stay dark. */
.est-hero-badge img{ width:26px; height:26px; filter: brightness(0) invert(1); }
.est-hero-breadcrumb{
  font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  display:flex; align-items:center; gap:8px; flex-wrap:wrap; color:rgba(255,255,255,.5);
}
.est-hero-breadcrumb a{ color:rgba(255,255,255,.65); }
.est-hero-breadcrumb a:hover{ color:#fff; text-decoration:underline; }
.est-hero-breadcrumb .current{ color:#8fb6e2; }
.est-hero-breadcrumb .sep{ opacity:.4; }

.est-calc-wrap{ position:relative; z-index:5; margin-top:-64px; }

/* Wraps the Method & theory / Related tools section below the calculator --
   was Bootstrap's plain py-5 (48px top and bottom); the calculator->theory
   gap needed more air than that, so this owns the section's full vertical
   padding instead of adding a competing padding-top utility (Bootstrap's
   utilities are !important -- see the Feature-page Hero Backgrounds note in
   CLAUDE.md for the bug this exact pattern caused once already). Bottom
   padding is unchanged from py-5. */
.est-theory-section{ padding-top:96px; padding-bottom:48px; }

.est-theory-card{ background:#fff; border:1px solid var(--line); border-radius:16px; padding:40px; }
.est-card-eyebrow{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--accent); }

.est-sidebar{ display:flex; flex-direction:column; gap:20px; }
.est-sidebar-card{ background:#fff; border:1px solid var(--line); border-radius:16px; padding:26px; }
.est-related-list{ margin-top:8px; }
.est-related-list a{
  display:flex; align-items:center; justify-content:space-between; gap:12px; padding:13px 10px;
  border-radius:9px; font-size:15px; font-weight:600; color:var(--navy);
}
.est-related-list a:hover{ background:var(--bg); color:var(--accent); }
.est-related-list a i{ font-size:12px; color:var(--mute); }

.est-bridge-card{ background:var(--navy); border:1px solid var(--navy); border-radius:16px; padding:30px; color:#fff; }
.est-bridge-card h3{ font-family:'Saira Semi Condensed',sans-serif; font-weight:700; font-size:24px; text-transform:uppercase; color:#fff; margin:12px 0 14px; }
.est-bridge-card p{ font-size:14.5px; color:rgba(255,255,255,.72); line-height:1.6; }
.est-bridge-card em{ font-style:normal; color:var(--accent-tint); }
.est-bridge-card .btn-accent{ margin-top:4px; }

@media (max-width:991.98px){
  .est-calc-wrap{ margin-top:24px; padding-left:0; padding-right:0; }
  .est-hero{ padding-bottom:40px; }
  .est-theory-section{ padding-top:56px; }
}
