/* ============================================================
   TransferTed Portal: portal.css
   Loads AFTER transferted-theme.css and site.css.

   NAMESPACE, stated honestly and completely, because this file loads last and a
   bare global selector in it repaints the whole application.

   EVERY selector in this file carries a .ttp- token. A number of them name a
   class from outside the namespace, and every one of those is ANCHORED to a
   .ttp- class in the same selector, in four families:

     .navbar.ttp-nav, .ttp-nav .nav-link, .ttp-nav .navbar-brand img,
     .ttp-nav .navbar-collapse, .ttp-nav .navbar-toggler, .ttp-nav__actions .btn,
     .ttp-nav__links .nav-link, .ttp-nav--member .nav-link,
     .ttp-nav--member .navbar-collapse
                          the rules that have to reach Bootstrap's own navbar
                          markup. All anchored on .ttp-nav, .ttp-nav--member or
                          .ttp-nav__*.
     .ttp-menu.dropdown-menu, .ttp-menu .dropdown-item, .ttp-menu .dropdown-divider
                          the shell's two dropdowns, and no other dropdown in the
                          application. All anchored on .ttp-menu.
     .ttp-alert.alert, .ttp-alert--*.alert, .ttp-alert .btn-close,
     .ttp-alert__action.btn
                          the page message, which dresses Bootstrap's own .alert
                          rather than replacing it, because data-bs-dismiss finds
                          what to close by looking for .alert. Anchored on .ttp-alert.
     .ttp-guardrail .bi, .ttp-hero__proof .bi, .ttp-list .bi,
     .ttp-menu .dropdown-item .bi, .ttp-alert__action .bi
                          Bootstrap icons, each anchored on its .ttp- parent.
     .ttp-reveal--armed.is-visible
                          the reveal's own state class, set by portal-home.js.

   Two selectors are not prefixed at all, and both are deliberate:

     html.ttp-scroll      scroll-behavior does not inherit from body to the
                          viewport, so it has to sit on <html>. The layout only
                          stamps .ttp-scroll there for a full-bleed portal page,
                          so paging and wizard navigation everywhere else keep
                          jumping instantly, as they did before this file existed.
     [class*="ttp-"]:not(html)
                          inside the prefers-reduced-motion block ONLY. Broad on
                          purpose, and explained where it is written. :not(html)
                          is there because <html> itself carries .ttp-scroll.

   Element selectors (span, p, a, img) all sit under a .ttp- ancestor. There is
   no unqualified element, id or Bootstrap-class selector anywhere in this file.
   If you add one, it will repaint screens you have never opened.

   Colour comes from the --mv-* tokens in transferted-theme.css. The one new
   colour token is the payroll violet, which exists so Ted and Tessa are told
   apart at a glance, and its neutral tints.

   GREEN IS THE AI MARK, not a general accent. It stays on the AI phase and its
   tag, the dry-run guardrail, the hero's automation proofs, the chips that say
   this person may actually run a stream, the wordmark's full stop (a fill, never
   a letterform) and the focus ring. Engine identity is carried by charcoal for
   Ted and violet for Tessa instead, which is what the dashboard's own head rule
   already did. Never set green as text on a light ground: #00D639 measures
   1.97:1 on white.
   ============================================================ */

:root {
  /* Tessa's accent. Chosen to sit beside Sage green without competing with it:
     green stays the brand, violet is only an identifier. */
  --ttp-payroll:      #7A5AF8;   /* rules and fills on DARK grounds */
  --ttp-payroll-deep: #5B3DD6;   /* chip fills on LIGHT grounds, 6.81:1 against white */
  --ttp-payroll-tint: #F1EDFE;   /* pale wash */

  /* Neutral tints of the existing charcoal, for the dark grounds. */
  --ttp-night:        #121212;
  --ttp-hair-dark:    rgba(255, 255, 255, 0.10);
  --ttp-text-dark-1:  rgba(255, 255, 255, 0.92);   /* 14.87:1 on --mv-ink */
  --ttp-text-dark-2:  rgba(255, 255, 255, 0.62);   /*  7.32:1 on --mv-ink */
  --ttp-text-dark-3:  rgba(255, 255, 255, 0.48);   /*  4.91:1 on --mv-ink; 0.45 measured 4.47:1 */

  --ttp-radius:       14px;
  --ttp-radius-lg:    20px;

  /* ── Type scale ────────────────────────────────────────────
     Seven steps, and every non-fluid font-size in this file is one of them.
     The file used to carry 21 ad hoc values, fourteen of them inside three
     clusters a reader cannot tell apart (five sizes across 1.2px, five across
     1.6px, four across 2px). Nothing visible changed when they collapsed: the
     largest move is 0.05rem, except the smallest step, which was deliberately
     lifted off an 11px floor that uppercase tracked labels could not carry.

     Fluid display type (the hero title, the section h2, the engine name, the
     dashboard greeting) stays on its own clamp() and is not part of the scale. */
  --ttp-fs-xs:        0.78rem;   /* 12.5px  eyebrows, chips, tags, footer headings */
  --ttp-fs-sm:        0.85rem;   /* 13.6px  meta, asides, notes, card descriptions */
  --ttp-fs-md:        0.95rem;   /* 15.2px  body copy */
  --ttp-fs-lg:        1.05rem;   /* 16.8px  card headings, lead-ins, large buttons */
  --ttp-fs-xl:        1.15rem;   /* 18.4px  panel titles */
  --ttp-fs-2xl:       1.35rem;   /* 21.6px  dashboard block headings */
  --ttp-fs-display:   1.6rem;    /* 25.6px  the type wordmark, and nothing else */

  /* One measure for prose that would otherwise run the full shell. */
  --ttp-measure:      65ch;

  /* ── Elevation ─────────────────────────────────────────────
     Five steps, and every box-shadow on a raised surface in this file is one of
     them. The file used to carry six ad hoc shadows, each invented at the rule
     that needed it, and a reader could not tell an engine card from a phase
     card from a chip because the ramp between them was arbitrary.

     Each step is TWO shadows, not one, because that is what a lit object
     actually casts: a tight, dark contact shadow directly under the object, and
     a wide, faint ambient shadow further out. A single blurred shadow is the
     flat-design tell; the pair is what reads as an object resting on a page
     rather than a rectangle with a grey edge.

     Opacity climbs and blur widens together as the object rises, which is the
     only way elevation reads as height rather than as a darker border. The
     colour is the ink's own blue-grey rather than pure black: a black shadow on
     a white ground reads as dirt, a tinted one as shade. */
  --ttp-elev-1:  0 1px 1px rgba(16, 24, 40, 0.04),
                 0 2px 6px -1px rgba(16, 24, 40, 0.05);
  --ttp-elev-2:  0 1px 2px rgba(16, 24, 40, 0.06),
                 0 6px 14px -3px rgba(16, 24, 40, 0.08);
  --ttp-elev-3:  0 2px 4px rgba(16, 24, 40, 0.06),
                 0 14px 28px -8px rgba(16, 24, 40, 0.10);
  --ttp-elev-4:  0 3px 6px rgba(16, 24, 40, 0.07),
                 0 28px 48px -16px rgba(16, 24, 40, 0.16);
  --ttp-elev-5:  0 4px 8px rgba(16, 24, 40, 0.08),
                 0 44px 72px -24px rgba(16, 24, 40, 0.22);

  /* The lit top edge, and it is a BORDER COLOUR rather than an inset shadow for
     a measured reason. The obvious way to light a card's top facet is
     `inset 0 1px 0 rgba(255,255,255,.92)`, but an inset white hairline is
     invisible on a white card, so it only works if the card body is washed
     slightly off-white first. Measured, that wash cost real contrast: the muted
     body copy on a #FCFDFC card bottom falls to 4.74:1, down from 4.83:1 on
     pure white. It still passes AA, and it is still headroom spent on an effect
     nobody can consciously see.

     A lighter TOP border buys the same cue for nothing. It sits outside the text
     area, so it cannot affect a single contrast measurement on the page, and
     against the section's --mv-snow ground it reads exactly as the top edge
     catching the light while the sides stay in shade. Cards keep pure white. */
  --ttp-edge-lit: #F1F3F5;

  /* The inverse, for surfaces that are meant to read as RECESSED rather than
     raised: the shadow falls inside the box from the top. Used on the quiet
     pricing panel, which is a well in the page rather than a card on it. */
  --ttp-inset:   inset 0 1px 3px rgba(16, 24, 40, 0.06),
                 inset 0 0 0 1px rgba(16, 24, 40, 0.02);

  /* One easing curve for every lift in this file, so a card, a panel and a
     button all rise with the same physics. Exponential ease-out: fast departure,
     long settle, no overshoot. Bounce on a business portal reads as a toy. */
  --ttp-ease:    cubic-bezier(0.16, 1, 0.3, 1);
  --ttp-lift:    260ms;
}

/* Smooth scrolling for the landing page's own anchors ONLY.
   scroll-behavior is not inherited from body to the viewport, so it has to sit
   on <html>; the layout stamps .ttp-scroll there for a full-bleed portal page,
   which keeps DataTables paging and wizard navigation in the rest of the
   application jumping instantly, as they did before this file existed. */
html.ttp-scroll { scroll-behavior: smooth; }

/* Anchored sections must clear the sticky navbar when jumped to. This used to
   be a bare [id], which matched every element carrying an id in the entire
   application, modals and overflow containers included. */
.ttp-section[id],
.ttp-hero[id] { scroll-margin-top: calc(var(--mv-navbar-h, 57px) + 1.25rem); }

/* ── Measure ────────────────────────────────────────────────
   Exactly the width of .container-fluid.px-4 > .row > .col-lg-9, which is what
   the navbar and every content page already use, so a full-bleed section's
   contents line up with the nav above it to the pixel. */
.ttp-shell {
  width: 100%;
  margin-inline: auto;
  padding-inline: 1.5rem;
}

@media (min-width: 992px) {
  .ttp-shell {
    width: calc((100% - 3rem) * 0.75);
    padding-inline: 0;
  }
}

/* Full-bleed page body: the landing page paints its own sections edge to edge,
   so the layout drops the container and the padding. */
.ttp-main { width: 100%; }

/* ── Navigation ─────────────────────────────────────────────
   The navbar is charcoal at rest, which is the same colour the hero starts on,
   so at the top of the landing page the two read as one unbroken field. Once
   the page scrolls it gains a hairline and a shadow and lifts off the content.
   .is-scrolled is set by portal-home.js; if that never runs the bar simply
   stays as it is, which is the state it shipped in. */
.navbar.ttp-nav {
  background: var(--mv-ink) !important;
  border-bottom: 1px solid transparent;
  box-shadow: none;
  transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
}

.navbar.ttp-nav.is-scrolled,
.navbar.ttp-nav.is-open {
  background: rgba(18, 18, 18, 0.94) !important;
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom-color: var(--ttp-hair-dark);
  box-shadow: 0 14px 34px -24px rgba(0, 0, 0, 0.95);
}

.ttp-nav .navbar-brand img { height: 30px; width: auto; }

/* Centre group. Bootstrap's me-auto/ms-auto put it in the middle of the row. */
.ttp-nav__links { gap: 0.15rem; }

.ttp-nav .nav-link {
  letter-spacing: -0.005em;
}

/* WHERE AM I. aria-current is the whole marker, and the theme's .active class is
   deliberately not used beside it: .active paints the link GREEN, and green is the AI
   mark, not a way of saying "this is the page you are standing on". White, heavier, on
   a faint pill instead. The landing page's section spy sets the same attribute as you
   scroll, so "the page I am on" and "the section I am in" read identically. */
.ttp-nav .nav-link[aria-current="true"],
.ttp-nav .nav-link[aria-current="page"] {
  color: #FFFFFF !important;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.10);
}

/* ── How wide this bar actually is ──────────────────────────
   Measured in a browser at four widths, not estimated, because everything below
   turns on it. The bar lives inside col-lg-9 rather than the window, so that the
   wordmark, the links and the account line up with the page underneath them: that
   is 75% of the window less the container's padding, and it is the whole
   constraint.

     window   bar      a visitor needs   an administrator needs
     1440     1044     711               1018   (Workspaces and Logs grouped)
                                         1097   (the two of them flat: overflows)
     1366      989     711                912
     1200      864     711                912   (overflows)
     1024      732     711                854   (overflows)

   Three things follow, and each of them is one of the rules below. The two admin
   links are GROUPED, or the bar wraps even on a 1440 screen. The account's NAME
   comes off the bar until there is room for it. And signed in, the bar folds into
   the menu button at xl rather than lg, because at 1024 a signed-in bar needs 854px
   of a bar that has 732: no amount of tightening closes a 122px gap, and wrapping
   is not an option. A visitor's four links still fit at 1024 with 21px to spare, so
   a visitor's bar folds where it always did. */

/* Applied to the links group only, so the actions on the right keep the theme's own
   spacing, and only from lg up, where the row is horizontal. */
@media (min-width: 992px) {
  .ttp-nav__links .nav-link { padding-inline: 0.62rem !important; }
}

/* 1200 to 1320 is the one band where a signed-in administrator's bar is genuinely
   tight: 912px wanted against 864 at 1200. Padding and gap close it with 45px in
   hand. Above 1320 the bar has room for the theme's own spacing again. */
@media (min-width: 1200px) and (max-width: 1319.98px) {
  .ttp-nav--member .ttp-nav__links { gap: 0; }
  .ttp-nav--member .ttp-nav__links .nav-link {
    padding-inline: 0.4rem !important;
    font-size: 0.86rem;
  }
}

/* Below 1500 the account's NAME comes off the bar and the disc carries the account
   on its own. Nothing is lost: the name is the first thing in the menu the disc
   opens, and it is what keeps an administrator's bar off the wrap. */
@media (min-width: 992px) and (max-width: 1499.98px) {
  .ttp-account__name { display: none; }
}

.ttp-nav__actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Account menu: a green disc carrying the initial, then the person's name.
   The disc was neutral for one release, on the argument that whose account this is has
   nothing to do with AI. It is green again by request, and it stays inside the brand
   rule because a green DISC is a fill: the letterform on it is charcoal, never green
   on a light ground. #00D639 measures 8.85:1 against the charcoal bar and the ink
   initial measures 8.85:1 on the disc. */
.ttp-nav .nav-link.ttp-account {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

.ttp-account__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--mv-blue);
  color: var(--mv-ink);
  font-weight: 800;
  font-size: var(--ttp-fs-sm);
  line-height: 1;
  flex: 0 0 auto;
}

/* Capped, because a person's name is the one thing on this bar nobody here chooses.
   At 16ch a long one could spend 28px the bar does not have at 1500. */
.ttp-account__name {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Ghost button for a dark ground. Not .btn-outline-secondary: that variant is
   built for paper and goes invisible on charcoal. */
.ttp-btn-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  font-family: var(--mv-font-body);
  font-size: var(--ttp-fs-md);
  font-weight: 600;
  line-height: 1.2;
  color: var(--ttp-text-dark-1);
  background: transparent;
  /* 3.22:1 against the hero ground. At the old 0.22 it measured 2.04:1 and the
     button read as floating text beside the solid primary, which flattened the
     two-action hierarchy the hero is built on. */
  border: 1.5px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--bs-border-radius);
  padding: 0.5rem 1.1rem;
  text-decoration: none;
  transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, transform 0.18s ease;
}

.ttp-btn-ghost:hover,
.ttp-btn-ghost:focus-visible {
  color: #FFFFFF;
  border-color: rgba(255, 255, 255, 0.5);
  background: rgba(255, 255, 255, 0.07);
}

/* The large call-to-action size, layered on top of the theme's .btn variants so
   focus rings, colours and disabled states all stay the theme's. */
.ttp-btn-xl {
  font-size: var(--ttp-fs-lg) !important;
  padding: 0.85rem 1.6rem !important;
  border-radius: 12px !important;
}

/* The toggler is the FIRST thing a thumb reaches on a phone and was the only target
   left under 44px: the theme's 0.25rem/0.55rem padding measures 40 x 50px. Outside
   any media query because Bootstrap only ever displays it while the bar is folded,
   and the bar now folds at two different widths depending on who is signed in.
   Namespaced on .ttp-nav so it cannot reach another navbar. */
.ttp-nav .navbar-toggler {
  min-height: 44px;
  min-width: 44px;
}

@media (max-width: 991.98px) {
  /* Tap targets: the theme's 0.4rem nav padding is a 34px target on a phone. */

  .ttp-nav .navbar-collapse {
    padding: 0.75rem 0 1rem;
    border-top: 1px solid var(--ttp-hair-dark);
    margin-top: 0.75rem;
  }

  .ttp-nav .nav-link {
    min-height: 44px;
    display: flex;
    align-items: center;
    padding: 0.6rem 0.75rem !important;
  }

  .ttp-nav__actions {
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
    padding: 0.75rem 0.75rem 0;
    margin-top: 0.5rem;
    border-top: 1px solid var(--ttp-hair-dark);
  }

  .ttp-nav__actions .btn,
  .ttp-nav__actions .ttp-btn-ghost {
    width: 100%;
    min-height: 44px;
  }

  /* Collapsed, the account is a row like any other, so the name comes back at full
     length and the menu it opens is a static block rather than a floating card. */
  .ttp-nav .nav-link.ttp-account { padding-inline: 0.75rem !important; }
  .ttp-account__name { max-width: none; }

  .ttp-menu.dropdown-menu {
    box-shadow: none;
    margin-top: 0.35rem;
  }
}

/* THE SAME FOLDED BAR, 200px WIDER. A signed-in bar folds at xl, so between 992 and
   1200 it is the menu-button bar and needs the menu-button styling: without this it
   would be folded and still wearing the horizontal bar's spacing. Every declaration
   here is the one directly above it, scoped to the signed-in bar. Duplicated rather
   than merged because CSS has no way to say "this block, at either of two widths",
   and the alternative, folding a visitor's bar at xl too, would change a signed-out
   nav that is right as it is. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .ttp-nav--member .navbar-collapse {
    padding: 0.75rem 0 1rem;
    border-top: 1px solid var(--ttp-hair-dark);
    margin-top: 0.75rem;
  }

  .ttp-nav--member .nav-link {
    min-height: 44px;
    display: flex;
    align-items: center;
    padding: 0.6rem 0.75rem !important;
  }

  .ttp-nav--member .ttp-nav__actions {
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
    padding: 0.75rem 0.75rem 0;
    margin-top: 0.5rem;
    border-top: 1px solid var(--ttp-hair-dark);
  }

  .ttp-nav--member .ttp-nav__actions .btn,
  .ttp-nav--member .ttp-nav__actions .ttp-btn-ghost {
    width: 100%;
    min-height: 44px;
  }

  .ttp-nav--member .nav-link.ttp-account { padding-inline: 0.75rem !important; }

  .ttp-nav--member .ttp-menu.dropdown-menu {
    box-shadow: none;
    margin-top: 0.35rem;
  }

  /* The name is on a row of its own here, not on a crowded horizontal bar, so the
     rules that shorten it and then take it off below 1500 must not reach into the
     folded menu. */
  .ttp-nav--member .ttp-account__name {
    display: inline;
    max-width: none;
  }
}

/* ── Menus ──────────────────────────────────────────────────
   The two dropdowns the shell owns: the administrator's Admin group and the account
   menu. The theme's own .dropdown-menu is close, and this only takes it the last step,
   inside the .ttp-menu namespace so no other dropdown in the application moves.

   The one real correction is the hover state. The theme paints a hovered item
   #00B32F on #E6FFF0, which measures 2.66:1 and is a green letterform on a light
   ground twice over. Charcoal on the app's own snow instead: 15.4:1. */
.ttp-menu.dropdown-menu {
  min-width: 15rem;
  padding: 0.4rem;
  border-radius: var(--ttp-radius);
  border-color: var(--mv-silver);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 22px 44px -26px rgba(16, 24, 40, 0.7);
}

.ttp-menu .dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;                /* a thumb target, on a phone and on a desk */
  padding: 0.5rem 0.7rem;
  border-radius: 9px;
  color: var(--mv-graphite);
  font-size: var(--ttp-fs-sm);
  font-weight: 500;
}

.ttp-menu .dropdown-item:hover,
.ttp-menu .dropdown-item:focus {
  background: var(--mv-snow);
  color: var(--mv-ink);
}

.ttp-menu .dropdown-item .bi {
  font-size: 1rem;
  color: var(--mv-iron);
  line-height: 1;
}

.ttp-menu .dropdown-item:hover .bi,
.ttp-menu .dropdown-item:focus .bi { color: var(--mv-ink); }

.ttp-menu .dropdown-divider {
  margin: 0.35rem 0.2rem;
  border-color: var(--mv-silver);
}

/* Sign out is a form button, so it is not an <a> and inherits nothing from one. */
.ttp-menu form { margin: 0; }

/* Whose account this is, at the top of the menu. It is the only place the full name
   is guaranteed to be readable: between 992 and 1400 the bar shows the disc alone. */
.ttp-menu__who {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem 0.7rem;
  margin-bottom: 0.2rem;
  border-bottom: 1px solid var(--mv-silver);
}

.ttp-menu__name {
  color: var(--mv-ink);
  font-weight: 700;
  font-size: var(--ttp-fs-md);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Page message ───────────────────────────────────────────
   The one-shot TempData message, rendered by _Alerts into both layouts. This dresses
   bootstrap's own .alert rather than replacing it: the markup keeps .alert so that
   data-bs-dismiss can still find what to close, and keeps the bootstrap variant class
   so the dark auth shell, which does not load this file, is unchanged.

   The band itself has NO vertical padding. It renders on every page, message or no
   message, so padding there would push every page in the application down by its own
   height and, on the full-bleed band, paint the ground as a visible stripe. Spacing
   belongs to the alert, and display: flow-root is what keeps that workable: without
   it the alert's own margins collapse straight out through the band, taking the dark
   ground off the message it is supposed to be behind. Measured both ways: the band is
   112px tall carrying one message and exactly 0 carrying none. */
.ttp-alerts { display: flow-root; }

.ttp-alerts--bleed {
  padding-inline: 1.5rem;   /* the shell's own gutter: a content page has main's px-4 */
  background: var(--mv-ink);
}

.ttp-alert.alert {
  position: relative;
  /* The markup carries .gap-2 so the auth shell, which never loads this file, still
     separates the mark from the message. Bootstrap writes its utilities !important, so
     widening it here has to answer in the same voice. */
  gap: 0.85rem !important;
  width: 100%;
  /* A short message must not become a stripe across a 1920px screen, and the auto
     margins are what put it in the same place on a page that centres its column and a
     page that does not. 44rem is not an arbitrary measure: it is exactly .tt-upload,
     so on the uploads screen, where a client lands after their first sign-in, the
     message sits precisely on the page's own column. */
  max-width: 44rem;
  margin: 0 auto 1.25rem;
  padding: 1rem 3.25rem 1rem 1.1rem;   /* room on the right for the dismiss button */
  border: 1px solid var(--mv-silver);
  border-radius: var(--ttp-radius);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 10px 22px -20px rgba(16, 24, 40, 0.6);
  color: var(--mv-ink);
  font-size: var(--ttp-fs-md);
}

.ttp-alerts--bleed .ttp-alert.alert { margin-block: 1.5rem; }

/* Ground per tone. Charcoal text on all four: 16.5:1 on the palest of them. */
.ttp-alert--success.alert { background: var(--mv-blue-lt); }
.ttp-alert--error.alert   { background: var(--mv-danger-lt); }
.ttp-alert--warning.alert { background: var(--mv-warning-lt); }
.ttp-alert--info.alert    { background: var(--mv-snow); }

/* The mark is a filled disc with a charcoal glyph, which is the brand rule for green:
   a fill, never a letterform. It is also the only shape that carries every tone at
   contrast. Green on its own tint measures 1.87:1 and amber measures 2.02:1, so a
   coloured glyph on a coloured ground was never an option here. */
.ttp-alert__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--mv-ink);
}

.ttp-alert--success .ttp-alert__mark { background: var(--mv-blue); }      /* 8.85:1 */
.ttp-alert--error .ttp-alert__mark   { background: var(--mv-danger); }    /* 4.62:1 */
.ttp-alert--warning .ttp-alert__mark { background: var(--mv-warning); }   /* 8.10:1 */
.ttp-alert--info .ttp-alert__mark    { background: var(--mv-slate); }     /* 11.98:1 */

.ttp-alert__body { min-width: 0; }

.ttp-alert__text {
  margin: 0;
  color: var(--mv-ink);
  line-height: 1.5;
}

/* The next action. This is what an administrator sees the moment a workspace or an
   account exists, so it is a real button and not a link wearing one: charcoal, because
   on a green-tinted ground a green button is a shape you have to look for. */
.ttp-alert__action.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 38px;
  padding: 0.4rem 0.9rem;
  border-radius: 9px;
  background: var(--mv-ink);
  border-color: var(--mv-ink);
  color: var(--mv-white);
  font-weight: 600;
  font-size: var(--ttp-fs-sm);
}

.ttp-alert__action.btn:hover,
.ttp-alert__action.btn:focus-visible {
  background: var(--mv-graphite);
  border-color: var(--mv-graphite);
  color: var(--mv-white);
}

.ttp-alert__action .bi { font-size: 0.9rem; line-height: 1; }

/* Dismiss. Bootstrap's own is a 24px target with a 1em glyph; this is a 44px target
   with the same glyph, which is the difference between a control and a speck. */
.ttp-alert .btn-close {
  top: 0.45rem;
  right: 0.45rem;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 10px;
  background-size: 0.7em;
  opacity: 0.55;
}

.ttp-alert .btn-close:hover,
.ttp-alert .btn-close:focus-visible {
  background-color: rgba(16, 24, 40, 0.06);
  opacity: 1;
}

@media (max-width: 575.98px) {
  .ttp-alert.alert {
    gap: 0.7rem;
    padding: 0.9rem 3rem 0.9rem 0.9rem;
  }
}

/* ── Sections ───────────────────────────────────────────────
   One vertical rhythm for the whole page, fluid between a phone and a desk. */
.ttp-section {
  padding-block: clamp(3.5rem, 7vw, 6.5rem);
}

.ttp-section--paper { background: var(--mv-white); }
.ttp-section--light { background: var(--mv-snow); }

.ttp-section--dark {
  background: var(--mv-ink);
  color: var(--ttp-text-dark-1);
}

.ttp-section__head {
  max-width: 46rem;
  margin-bottom: clamp(2rem, 4vw, 3.25rem);
}

/* Eyebrow: a charcoal dot, then a quiet uppercase label.
   The dot was green, on every section label including Pricing and Support. A
   mark that appears on all five headings marks nothing. */
.ttp-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-size: var(--ttp-fs-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mv-iron);   /* 4.63:1 on --mv-snow, 4.83:1 on --mv-white */
  margin-bottom: 1rem;
}

.ttp-eyebrow::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--mv-ink);
  flex: 0 0 auto;
}

.ttp-section--dark .ttp-eyebrow { color: var(--ttp-text-dark-3); }
.ttp-section--dark .ttp-eyebrow::before { background: var(--ttp-text-dark-3); }

.ttp-h2 {
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  font-weight: 800;
  letter-spacing: -0.032em;
  line-height: 1.08;
  color: var(--mv-ink);
  margin-bottom: 0.9rem;
}

.ttp-section--dark .ttp-h2 { color: #FFFFFF; }

.ttp-sub {
  font-size: clamp(1rem, 1.15vw, 1.075rem);
  line-height: 1.75;
  color: var(--mv-iron);
  max-width: 38rem;
  margin-bottom: 0;
}

.ttp-section--dark .ttp-sub { color: var(--ttp-text-dark-2); }

/* The wordmark set as type, for light grounds only. Display sized, never body.
   Every letterform is charcoal, at 17.40:1 on white. */
.ttp-wordmark {
  font-family: var(--mv-font-heading);
  font-weight: 800;
  letter-spacing: -0.04em;
  color: var(--mv-ink);
  line-height: 1;
}

/* The signature full stop, and the one green thing on this panel. It is a
   SHAPE, not a glyph: the markup carries no full stop at all, so there is no
   green letterform anywhere on a light ground. Set as type it measured 1.97:1
   on white, which is the brand rule and the contrast rule failing together. */
.ttp-wordmark__dot {
  display: inline-block;
  width: 0.26em;
  height: 0.26em;
  margin-left: 0.05em;
  border-radius: 50%;
  background: var(--mv-blue);
  vertical-align: baseline;
}

/* Display sized, which is the only size the text wordmark is allowed at. */
.ttp-wordmark--md { font-size: var(--ttp-fs-display); margin-bottom: 1rem; }

/* ── Hero ───────────────────────────────────────────────────
   Starts on exactly the navbar's charcoal so there is no seam, then sinks. */
.ttp-hero {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, var(--mv-ink) 0%, var(--ttp-night) 45%, #0C0F0D 100%);
  color: var(--ttp-text-dark-1);
  padding-block: clamp(4rem, 9vw, 8rem) clamp(4.5rem, 10vw, 8.5rem);
}

/* One soft green bloom, top right. Pure decoration, and it never takes clicks. */
.ttp-hero__glow {
  position: absolute;
  /* The box's TOP edge sits exactly on the hero's, and a closest-side gradient is
     fully transparent along that edge, so the glow cannot draw a seam where the
     hero meets the navbar. Its centre lands behind the headline instead. */
  top: 0;
  right: -20%;
  width: min(1000px, 115vw);
  height: min(1000px, 115vw);
  /* closest-side finishes the gradient exactly on the box edge, so the element's
     own bounds can never show as a straight line across the hero. */
  background: radial-gradient(closest-side, rgba(0, 214, 57, 0.22), rgba(0, 214, 57, 0));
  pointer-events: none;
}

.ttp-hero__inner { position: relative; }

.ttp-hero__title {
  font-size: clamp(2.15rem, 6vw, 4.6rem);
  font-weight: 800;
  letter-spacing: -0.042em;
  line-height: 1.03;
  color: #FFFFFF;
  /* Holds the headline to three lines on a desk without a manual break, which
     would land in the wrong place on every other width. */
  max-width: 23ch;
  margin: 0 0 clamp(1.25rem, 2.2vw, 1.75rem);
}

.ttp-hero__lede {
  font-size: clamp(1rem, 1.35vw, 1.2rem);
  line-height: 1.75;
  color: var(--ttp-text-dark-2);
  max-width: 64ch;
  margin-bottom: clamp(1.75rem, 3vw, 2.5rem);
}

.ttp-hero__lede strong {
  color: var(--ttp-text-dark-1);
  font-weight: 600;
}

.ttp-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
}

/* Three short proofs under the buttons, each led by a green tick. */
.ttp-hero__proof {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--ttp-hair-dark);
  list-style: none;
  padding-left: 0;
  margin-bottom: 0;
}

.ttp-hero__proof li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--ttp-fs-md);
  color: var(--ttp-text-dark-2);
}

/* Green KEPT. These three lines are the AI and automation promises, they are on
   the near-black hero ground where green measures 8.85:1, and they are the mark
   doing the job it exists for. */
.ttp-hero__proof .bi { color: var(--mv-blue); font-size: var(--ttp-fs-lg); }

/* ── Engine cards ───────────────────────────────────────────
   Two equal-height cards, each with a coloured top rule that says which engine
   it is before a word is read. They stack below 900px. */
.ttp-engines {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.4vw, 2rem);
}

/* The measure is 75% of the viewport, so two engine cards only get a readable
   column each from about 1140px up. Below that they stack rather than squeeze. */
@media (max-width: 1139.98px) {
  .ttp-engines { grid-template-columns: 1fr; }
}

.ttp-engine {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  /* The lit facet. See --ttp-edge-lit in the token block. */
  border-top-color: var(--ttp-edge-lit);
  border-radius: var(--ttp-radius-lg);
  padding: clamp(1.5rem, 2.6vw, 2.25rem);
  box-shadow: var(--ttp-elev-2);
  overflow: hidden;
  transition: box-shadow var(--ttp-lift) var(--ttp-ease),
              transform var(--ttp-lift) var(--ttp-ease),
              border-color var(--ttp-lift) var(--ttp-ease);
}

/* Engine identity: charcoal for Ted, violet for Tessa. This pair is the ONE
   carrier of which engine a surface belongs to, and it is deliberately not the
   AI colour, so a reader is not told "accounting" and "AI" with the same mark.
   The dashboard's head rule already worked this way; the cards now agree. */
.ttp-engine::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: var(--mv-ink);
}

.ttp-engine--payroll::before { background: var(--ttp-payroll); }

/* THE LIFT. Elevation two to elevation five, which is a real change in height
   rather than a slightly darker blur: the contact shadow tightens and darkens
   while the ambient one widens, exactly as an object leaving a surface does.
   Three pixels of travel is enough to read as movement without the card
   appearing to jump out of the grid. */
.ttp-engine:hover {
  transform: translateY(-3px);
  border-color: var(--mv-slate);
  box-shadow: var(--ttp-elev-5);
}

.ttp-engine__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.9rem;
}

.ttp-engine__name {
  font-size: clamp(1.5rem, 2.2vw, 1.9rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--mv-ink);
  margin: 0;
}

.ttp-engine__blurb {
  color: var(--mv-iron);
  font-size: var(--ttp-fs-md);
  line-height: 1.7;
  margin-bottom: 1.5rem;
}

.ttp-engine__tracks {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  border-top: 1px solid var(--mv-silver);
}

/* This line is the engine's run shape, which is real copy a partner reads, not
   ornament. In --mv-mist it measured 2.43:1 on snow and was the faintest thing
   on the page; --mv-iron measures 4.63:1 on snow and 4.83:1 on white.
   --mv-mist is now reserved for non-text ornament. */
.ttp-engine__meta {
  margin-top: auto;
  padding-top: 1.1rem;
  border-top: 1px solid var(--mv-silver);
  font-size: var(--ttp-fs-sm);
  color: var(--mv-iron);
  margin-bottom: 0;
}

/* One stream, one row: the pair on the left, its state on the right. */
.ttp-track {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--mv-silver);
}

.ttp-track__pair {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-weight: 600;
  font-size: var(--ttp-fs-md);
  color: var(--mv-ink);
  min-width: 0;
}

.ttp-track:last-child { border-bottom: 0; }

.ttp-track--soon .ttp-track__pair { color: var(--mv-iron); font-weight: 500; }

/* Source, then arrow-and-target as ONE flex item. Written as three siblings the
   arrow stayed on the first line pointing at nothing while the target wrapped
   below it, which is what the primary product list did on a phone. */
.ttp-pairto {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.ttp-track__arrow {
  color: var(--mv-mist);   /* ornament, not type */
  font-size: var(--ttp-fs-sm);
  flex: 0 0 auto;
}

/* PHONE, and the fix is the TYPE SIZE rather than the layout, which is worth
   recording because the obvious answer measured worse.

   At 375px the longest rows ran to three lines: "VIP Premier / Classic /
   Essentials" alone overran the 239px left after the chip, so the source took
   two lines and the arrow-and-target pair took a third. Height 109px.

   Stacking the row into a column and putting the chip underneath looked like
   the tidy answer and measured 120px, WORSE, because the chip stopped sharing
   the source's line and started demanding one of its own.

   One step down the type scale is what actually fixes it: at 13.6px the longest
   source fits its line, the arrow and target take the second, the chip stays
   where it was, and the row comes back to two lines. The scale already has the
   step, so nothing ad hoc is introduced. Tightening the gap recovers the last
   few pixels the chip needs to stay out of the wrap. */
@media (max-width: 479.98px) {
  .ttp-track {
    gap: 0.6rem;
    align-items: flex-start;
  }

  .ttp-track__pair { font-size: var(--ttp-fs-sm); }

  /* The chip must never be the thing that wraps: it is the row's verdict. */
  .ttp-track .ttp-chip { flex: 0 0 auto; }
}

/* ── Chips ──────────────────────────────────────────────────
   Green is a fill with charcoal type on top, which is the brand rule and also
   the highest contrast pair we own. Never green type on paper. */
.ttp-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex: 0 0 auto;
  font-family: var(--mv-font-body);
  font-size: var(--ttp-fs-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1;
  padding: 0.42rem 0.7rem;
  border-radius: 50rem;
  white-space: nowrap;
}

/* Green KEPT. This chip means "you may actually run this", which is the closest
   thing on the dashboard to the mark's real job, and the view now derives it
   from the same value that decides whether the card is a link. */
.ttp-chip--live {
  background: var(--mv-blue);
  color: var(--mv-ink);   /* 8.85:1 */
}

/* Anything the reader cannot open. Text was --mv-iron at 4.39:1 on its own fog
   fill, and the border was --mv-silver at 1.19:1, so on a recessed card the
   pill lost its shape and the words floated as loose grey text. Graphite on fog
   measures 9.37:1, and an --mv-iron edge holds on white (4.83:1), on fog
   (4.39:1) and on the recessed card's slate ground (3.28:1). */
.ttp-chip--soon {
  background: var(--mv-fog);
  color: var(--mv-graphite);
  border: 1px solid var(--mv-iron);
}

/* Which ENGINE, not which state: charcoal for Ted, violet for Tessa, the same
   pair the card rules and the dashboard head rule use. It was green, which made
   the mark say "accounting" on one chip and "available" on the chip beside it. */
.ttp-chip--kind {
  background: var(--mv-ink);
  color: #FFFFFF;         /* 17.40:1 */
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.ttp-chip--kind-payroll {
  background: var(--ttp-payroll-deep);
  color: #FFFFFF;         /* 6.81:1 */
}

/* ── How a migration runs ───────────────────────────────────
   Five phases across a row on a desk, stacking down. Each card carries its own
   top rule, so the row reads as one segmented track. The AI phase is the green
   one, which is the whole of what green means here. */
.ttp-phases {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  counter-reset: ttp-phase;
}

@media (min-width: 620px) {
  .ttp-phases { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1140px) {
  .ttp-phases { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* There is deliberately NO five-across breakpoint. One was written at 1800px on
   the theory that a wider screen should show more of the sequence at once, and
   measured it did the opposite: the shell is 75% of the viewport, so three
   across gives 337px cards at 1440 and 457px at 1920, while five across drops
   them to 268px at 1920, about 29 characters a line, and does not recover the
   1600px width again until past 2560. A five-line paragraph became a nine-line
   ragged column at exactly the resolution a demonstration runs at. Three across
   is the widest row where each card still carries its sentence. */

.ttp-phase {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  border-top-color: var(--ttp-edge-lit);
  border-radius: var(--ttp-radius);
  padding: 1.5rem 1.35rem 1.35rem;
  counter-increment: ttp-phase;
  /* One step BELOW the engine cards, deliberately. Five phases at the same
     height as two engines would flatten the page into one plane of cards; the
     engines are the page's subject and the phases explain them, so the ramp
     says so before the copy does. */
  box-shadow: var(--ttp-elev-1);
  transition: transform var(--ttp-lift) var(--ttp-ease),
              box-shadow var(--ttp-lift) var(--ttp-ease);
}

.ttp-phase::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--mv-silver);
  border-radius: var(--ttp-radius) var(--ttp-radius) 0 0;
}

.ttp-phase:hover {
  transform: translateY(-3px);
  box-shadow: var(--ttp-elev-4);
}

/* The number is a PHYSICAL TOKEN sitting on the card, not a coloured circle
   printed on it: a hairline of light across its own top, a soft shadow beneath
   it, and the ring that separates it from the card it rests on. It is the
   smallest object on the page carrying real elevation, and it is what makes the
   five cards read as five steps rather than five paragraphs. */
.ttp-phase__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  /* THE SAME OBJECT AS STEP TWO, AND ONLY THE COLOUR DIFFERS. These four were a
     pale disc with graphite type and a hairline ring, while the AI badge was a
     saturated fill with a glow, so the five read as four labels and one button
     rather than as one sequence. Same gradient shape, same shadow structure,
     same lit top edge; charcoal fill with a white glyph instead of green fill
     with a charcoal one. The ring is gone, because a filled disc does not need
     an outline to separate it from the card. */
  background: linear-gradient(180deg, #3A3A3A 0%, var(--mv-ink) 100%);
  color: #FFFFFF;                /* 17.40:1 on the charcoal fill */
  font-weight: 800;
  font-size: var(--ttp-fs-md);
  margin-bottom: 0.9rem;
  /* Tabular figures so 1 and 4 occupy the same width and the five badges do not
     visibly disagree about their own centres. */
  font-variant-numeric: tabular-nums;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.12),
              0 6px 16px -4px rgba(16, 24, 40, 0.35),
              inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.ttp-phase__num::before { content: counter(ttp-phase); }

.ttp-phase h3 {
  font-size: var(--ttp-fs-lg);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--mv-ink);
  margin-bottom: 0.45rem;
}

.ttp-phase p {
  font-size: var(--ttp-fs-md);
  line-height: 1.65;
  color: var(--mv-iron);
  margin-bottom: 0;
}

/* The one AI phase, and green KEPT: this is the whole of what the mark means. */
.ttp-phase--ai::before { background: var(--mv-blue); }

.ttp-phase--ai .ttp-phase__num {
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);   /* 8.85:1 */
  /* The one badge that is lit rather than merely raised: a green halo under it,
     because this is the AI step and the mark is allowed to glow here. */
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.12),
              0 6px 16px -4px rgba(0, 214, 57, 0.45),
              inset 0 1px 0 rgba(255, 255, 255, 0.55);
}

.ttp-phase__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  align-self: flex-start;
  margin-top: 0.85rem;
  font-size: var(--ttp-fs-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mv-ink);   /* 8.85:1 */
  background: var(--mv-blue);
  border-radius: 50rem;
  padding: 0.3rem 0.6rem;
}

/* The line that matters most on this section, so it gets its own rule, and green
   KEPT on that rule: this sentence IS the automation guardrail. */
.ttp-guardrail {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
  padding: 1.1rem 1.25rem;
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  border-left: 4px solid var(--mv-blue);
  border-radius: var(--ttp-radius);
  color: var(--mv-graphite);   /* 10.31:1 on white */
  font-size: var(--ttp-fs-md);
  line-height: 1.65;
}

/* Measured. Without this the single most important sentence on the page ran the
   full shell: 1317px at 15.2px is 162 characters a line at 1920, which is the
   resolution the demonstration runs at and more than double the readable band. */
.ttp-guardrail span { max-width: var(--ttp-measure); }

.ttp-guardrail .bi {
  color: var(--mv-ink);
  font-size: var(--ttp-fs-xl);
  line-height: 1.5;
  flex: 0 0 auto;
}

/* ── Pricing ────────────────────────────────────────────────
   No number appears anywhere in this block, by product rule. */
.ttp-pricing {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(1.25rem, 2.4vw, 2rem);
  align-items: stretch;
}

@media (max-width: 899.98px) {
  .ttp-pricing { grid-template-columns: 1fr; }
}

.ttp-panel {
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  border-top-color: var(--ttp-edge-lit);
  border-radius: var(--ttp-radius-lg);
  padding: clamp(1.5rem, 2.6vw, 2.25rem);
  box-shadow: var(--ttp-elev-2);
}

/* RECESSED, not raised, and that is the whole idea. The quiet panel used to say
   "secondary" with a dashed border, which is the convention for a drop zone or
   an empty state, not for standing copy. A well in the page says the same thing
   with real depth: the shadow falls INSIDE from the top, there is no lit lip,
   and the surface sits below the panel beside it rather than level with it.
   Raised and recessed as a pair is the clearest hierarchy two panels can have. */
.ttp-panel--quiet {
  background: var(--mv-snow);
  border-color: var(--mv-silver);
  box-shadow: var(--ttp-inset);
}

.ttp-panel__title {
  font-size: var(--ttp-fs-xl);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--mv-ink);
  margin-bottom: 0.75rem;
}

.ttp-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.ttp-list li {
  display: flex;
  gap: 0.7rem;
  padding: 0.6rem 0;
  font-size: var(--ttp-fs-md);
  line-height: 1.6;
  color: var(--mv-graphite);
  border-bottom: 1px solid var(--mv-silver);
}

.ttp-list li:last-child { border-bottom: 0; }

.ttp-list .bi {
  flex: 0 0 auto;
  margin-top: 0.15rem;
  color: var(--mv-ink);
}

/* A tick in a disc, so it is a fill rather than a coloured glyph. The disc was
   green, marking "this part is free", which is a commercial fact and not the
   AI. Charcoal, and the mark is left to mean one thing. */
.ttp-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.18rem;
  border-radius: 50%;
  background: var(--mv-ink);
  font-size: var(--ttp-fs-xs);
}

/* The glyph, and it needs the same specificity as .ttp-list .bi above or that rule
   wins and paints a charcoal tick onto a charcoal disc: 1:1, an invisible tick in a
   black circle. Verified in the browser, because the colour on .ttp-tick itself
   looked correct in the file and was being overridden. */
.ttp-tick .bi { color: #FFFFFF; }   /* 17.40:1 on the disc */

/* The pricing caveat, which is the commercial promise about when a token is
   spent. It was --mv-mist at 2.43:1 on snow and vanished on a projector. */
.ttp-note {
  font-size: var(--ttp-fs-sm);
  color: var(--mv-iron);   /* 4.63:1 on --mv-snow, 4.83:1 on --mv-white */
  margin-top: 1.1rem;
  margin-bottom: 0;
  max-width: var(--ttp-measure);
}

/* ── Support ────────────────────────────────────────────────*/
.ttp-support {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.4vw, 2rem);
}

@media (max-width: 767.98px) {
  .ttp-support { grid-template-columns: 1fr; }
}

.ttp-contact {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 1.1rem 0 0;
  padding: 0;
  list-style: none;
}

.ttp-contact li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--ttp-fs-md);
  color: var(--mv-graphite);
}

/* The 44px belongs on the ANCHOR, not the row. Sized on the li the row measured
   44px while the phone number inside it was a 24px target, and the phone number
   and the email address are the two things a partner is most likely to tap.
   Colour: --mv-blue-hover measured 2.68:1 on the panel's snow ground, which is
   green as a letterform on a light ground, twice forbidden. --mv-cyan is the
   theme's own deep-green secondary and measures 5.25:1 on snow, 5.48:1 on
   white, with a green underline carrying the brand as a rule. */
.ttp-contact a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--mv-cyan);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--mv-blue);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

.ttp-contact a:hover,
.ttp-contact a:focus-visible { color: var(--mv-cyan-hover); }

/* ── Closing band ───────────────────────────────────────────*/
.ttp-cta {
  position: relative;
  overflow: hidden;
  background: linear-gradient(135deg, var(--mv-ink) 0%, #14251A 100%);
  color: var(--ttp-text-dark-1);
  padding-block: clamp(3.5rem, 7vw, 6rem);
}

.ttp-cta__glow {
  position: absolute;
  left: -12%;
  bottom: -70%;
  width: min(820px, 100vw);
  height: min(820px, 100vw);
  background: radial-gradient(closest-side, rgba(0, 214, 57, 0.20), rgba(0, 214, 57, 0));
  pointer-events: none;
}

.ttp-cta__inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.ttp-cta__copy { max-width: 56ch; }

.ttp-cta .ttp-h2 { color: #FFFFFF; }
.ttp-cta .ttp-sub { color: var(--ttp-text-dark-2); margin-bottom: 0; }

/* ── Footer ─────────────────────────────────────────────────*/
.ttp-footer {
  background: var(--mv-ink);
  color: var(--ttp-text-dark-2);
  border-top: 1px solid #000000;
  padding-block: clamp(2.5rem, 5vw, 3.75rem) 0;
  margin-top: 0;
  flex-shrink: 0;
}

.ttp-footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
}

@media (max-width: 767.98px) {
  .ttp-footer__grid { grid-template-columns: 1fr; }
}

.ttp-footer__logo { height: 26px; width: auto; margin-bottom: 1rem; }

.ttp-footer h2 {
  font-size: var(--ttp-fs-xs);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ttp-text-dark-3);   /* 4.91:1 on --mv-ink */
  margin-bottom: 0.85rem;
}

.ttp-footer p {
  font-size: var(--ttp-fs-sm);
  line-height: 1.7;
  color: var(--ttp-text-dark-2);
  margin-bottom: 0.65rem;
  max-width: var(--ttp-measure);
}

.ttp-footer p:last-child { margin-bottom: 0; }

.ttp-footer a {
  color: var(--ttp-text-dark-1);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.25);
}

/* Green KEPT: a rule under a letterform is the brand's own sanctioned use, and
   this one is on the near-black footer where green measures 8.85:1. */
.ttp-footer a:hover { border-bottom-color: var(--mv-blue); }

/* The footer's one standalone link sits alone in its paragraph, so it can carry
   a real tap target without disturbing a line of prose. It measured 20px. */
.ttp-footer p a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.ttp-footer__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  padding-block: 1.1rem;
  border-top: 1px solid var(--ttp-hair-dark);
  font-size: var(--ttp-fs-sm);
  color: var(--ttp-text-dark-3);   /* 4.91:1 on --mv-ink */
}

.ttp-footer__ai {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.ttp-footer__ai img { height: 20px; width: auto; }

/* ── Dashboard ──────────────────────────────────────────────
   The same two-card language as the landing page, denser and workmanlike. */
.ttp-dash { padding-bottom: 1rem; }

.ttp-dash__head {
  padding-bottom: 1.5rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
  border-bottom: 1px solid var(--mv-silver);
}

.ttp-dash__greet {
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  font-weight: 800;
  letter-spacing: -0.032em;
  line-height: 1.1;
  color: var(--mv-ink);
  margin: 0 0 0.5rem;
}

.ttp-dash__meta {
  font-size: var(--ttp-fs-md);
  color: var(--mv-iron);
  margin-bottom: 0;
  max-width: var(--ttp-measure);
}

/* The "nothing on your plan yet" banner. Calm, not an alarm: this is our
   unfinished setup, not the client's mistake.

   Its rule and its disc were green, on a message that is neutral at best. Green
   conventionally reads as success, so the mark was saying "done" on the one
   panel that exists to say "not done yet". Charcoal and grey say it plainly. */
.ttp-banner {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  border-left: 4px solid var(--mv-graphite);
  border-radius: var(--ttp-radius);
  padding: 1.15rem 1.35rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

.ttp-banner__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background: var(--mv-fog);
  color: var(--mv-graphite);   /* 9.37:1 on --mv-fog */
  font-size: var(--ttp-fs-lg);
}

.ttp-banner__title {
  font-size: var(--ttp-fs-lg);
  font-weight: 700;
  color: var(--mv-ink);
  margin-bottom: 0.25rem;
}

.ttp-banner p {
  font-size: var(--ttp-fs-md);
  line-height: 1.65;
  color: var(--mv-iron);
  margin-bottom: 0;
  max-width: var(--ttp-measure);
}

.ttp-engineblock { margin-bottom: clamp(2rem, 4vw, 3rem); }

/* The head carries the engine's name and its kind chip, and nothing else. The
   run-shape line used to be a third flex item pushed right with margin-left:auto,
   which below about 700px wrapped to its own line and sat right-aligned and
   orphaned between the name and the rule, reading as an accident. It is a row of
   its own now, under the blurb, which is also what let the blurb's negative top
   margin go: two rules were fighting over one gap. */
.ttp-engineblock__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding-bottom: 0.85rem;
  margin-bottom: 0.85rem;
  border-bottom: 2px solid var(--mv-ink);
}

.ttp-engineblock--payroll .ttp-engineblock__head { border-bottom-color: var(--ttp-payroll-deep); }

.ttp-engineblock__name {
  font-size: var(--ttp-fs-2xl);
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--mv-ink);
  margin: 0;
}

/* Real copy, not ornament: this is the engine's run shape. --mv-mist measured
   2.43:1 on the dashboard's snow ground. */
.ttp-engineblock__meta {
  font-size: var(--ttp-fs-sm);
  color: var(--mv-iron);
  margin: 0 0 0.35rem;
}

.ttp-engineblock__blurb {
  font-size: var(--ttp-fs-md);
  line-height: 1.7;
  color: var(--mv-iron);
  max-width: var(--ttp-measure);
  margin: 0 0 1.25rem;
}

.ttp-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem;
}

@media (max-width: 575.98px) {
  .ttp-cards { grid-template-columns: 1fr; }
}

/* One stream card. Its whole appearance comes from the StreamAccess modifier
   the view stamps on it, and from nothing else. */
.ttp-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--mv-white);
  border: 1px solid var(--mv-silver);
  border-radius: var(--ttp-radius);
  padding: 1.35rem 1.4rem 1.2rem;
  text-decoration: none;
  color: inherit;
  min-height: 100%;
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}

.ttp-card__pair {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.45rem;
  font-size: var(--ttp-fs-lg);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--mv-ink);
  margin-bottom: 0.5rem;
}

.ttp-card__arrow { color: var(--mv-mist); font-size: var(--ttp-fs-sm); }

.ttp-card__desc {
  font-size: var(--ttp-fs-sm);
  line-height: 1.6;
  color: var(--mv-iron);   /* 4.83:1 on the open card's white ground */
  margin-bottom: 1.1rem;
}

.ttp-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: auto;
  padding-top: 0.9rem;
  border-top: 1px solid var(--mv-silver);
}

.ttp-card__open {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--ttp-fs-sm);
  font-weight: 600;
  color: var(--mv-graphite);
  transition: gap 0.18s ease, color 0.18s ease;
}

/* Open: full colour, a real link, sits slightly proud of the page, lifts on hover. */
.ttp-card--open {
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 10px 22px -20px rgba(16, 24, 40, 0.6);
}

/* Engine identity, not availability: charcoal for Ted, violet for Tessa, the
   same pair as the landing page's engine cards and this page's own head rule. */
.ttp-card--open::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  border-radius: var(--ttp-radius) var(--ttp-radius) 0 0;
  background: var(--mv-ink);
}

.ttp-engineblock--payroll .ttp-card--open::before { background: var(--ttp-payroll); }

.ttp-card--open:hover,
.ttp-card--open:focus-visible {
  transform: translateY(-4px);
  border-color: var(--mv-slate);
  box-shadow: 0 26px 44px -30px rgba(16, 24, 40, 0.45);
}

.ttp-card--open:hover .ttp-card__open {
  gap: 0.65rem;
  color: var(--mv-ink);
}

/* Shut: obviously, visibly out of reach from the back of a meeting room.

   Deliberately NOT dimmed with opacity. A client has to be able to READ the
   stream they did not buy, because reading it is the entire reason it is on the
   page, and stacking opacity on top of grey text took the description under 3:1.

   FIVE cues change at once, because one of them will always be the one the
   projector eats. The previous version claimed to be recessed "by ground, edge
   and rule" and measured: its --mv-fog ground was 1.05:1 against the page and
   its --mv-slate edge 1.41:1, so the whole distinction actually rested on a 3px
   line and one small pill.

     1. ground   --mv-slate, 1.41:1 against the snow page and 1.47:1 against an
                 open white card. A real step down rather than a tint.
     2. edge     dashed --mv-iron, 3.28:1 against its own ground, clearing the
                 3:1 a component boundary needs. It was --mv-slate: 1.19:1.
     3. relief   the drop shadow is replaced by an INSET one, so the card reads
                 as pressed into the page rather than lifted off it.
     4. rule     no coloured top rule at all.
     5. chip     grey with a real edge, never the green one.

   Text moves to --mv-graphite: 7.00:1 on this ground. --mv-iron would be 3.28:1
   here, which is the trap in darkening a ground and leaving the copy alone. The
   old filter: saturate(0.2) is gone with it. Its only real effect was draining a
   green "Available" chip on a card that could not be opened, and that state is
   now fixed at source in the view instead of being cosmetically hidden. */
.ttp-card--shut {
  background: var(--mv-slate);
  border-style: dashed;
  border-color: var(--mv-iron);
  box-shadow: inset 0 2px 4px rgba(16, 24, 40, 0.10);
  cursor: not-allowed;
}

.ttp-card--shut .ttp-card__pair { color: var(--mv-graphite); font-weight: 600; }   /* 7.00:1 */
.ttp-card--shut .ttp-card__desc,
.ttp-card--shut .ttp-card__aside { color: var(--mv-graphite); }                    /* 7.00:1 */
.ttp-card--shut .ttp-card__arrow { color: var(--mv-iron); }
.ttp-card--shut .ttp-card__foot { border-top-color: var(--mv-iron); }

.ttp-card__aside {
  font-size: var(--ttp-fs-sm);
  line-height: 1.55;
  color: var(--mv-iron);
  margin: 0.75rem 0 0;
}

/* ── Scroll reveal ──────────────────────────────────────────
   Everything is VISIBLE by default. portal-home.js only arms elements that are
   below the fold when it runs, so a JS failure, or a reader who has asked for
   less motion, simply sees the finished page. */
.ttp-reveal {
  transition: opacity 0.55s ease, transform 0.55s ease;
}

.ttp-reveal--armed {
  opacity: 0;
  transform: translateY(18px);
}

.ttp-reveal--armed.is-visible {
  opacity: 1;
  transform: none;
}

/* ── Reduced motion ─────────────────────────────────────────
   BROAD ON PURPOSE. This used to be an enumerated list of six selectors, and it
   had already drifted: it did not cover the page's three largest buttons,
   because the hero, closing-band and pricing calls to action are .btn-primary,
   whose transition and hover translate come from transferted-theme.css. That
   file has no reduced-motion block of its own, so nothing caught them. The list
   was true of the rules this FILE declares and false of the motion this file's
   own markup summons, which is the distinction a reader with vestibular
   sensitivity does not care about.

   So: everything the portal owns, and everything inside it.

   :not(html) is load bearing, and it was found by measuring rather than by
   reading. <html> itself carries .ttp-scroll on a full-bleed page, so without it
   the substring selector matched the root and "everything inside it" became the
   entire document, freezing the global page loader's spinner, the toast
   container and the confirm modal's fade. Worse, it did so on the landing page
   and not on the dashboard, so one reduced-motion setting produced two different
   behaviours depending on which page you were standing on. Excluding the root
   puts those three body-level elements back outside the portal's reach, where
   they were before this file existed.

   Adding a rule to this file can no longer leave a transition uncovered. */
@media (prefers-reduced-motion: reduce) {
  html.ttp-scroll { scroll-behavior: auto; }

  [class*="ttp-"]:not(html),
  [class*="ttp-"]:not(html)::before,
  [class*="ttp-"]:not(html)::after,
  [class*="ttp-"]:not(html) *,
  [class*="ttp-"]:not(html) *::before,
  [class*="ttp-"]:not(html) *::after {
    transition: none !important;
    animation: none !important;
  }

  /* transform is a separate property: removing the transition leaves the hover
     translate in place and it simply jumps instead of sliding. */
  [class*="ttp-"]:not(html):hover,
  [class*="ttp-"]:not(html):focus-visible,
  [class*="ttp-"]:not(html) *:hover,
  [class*="ttp-"]:not(html) *:focus-visible {
    transform: none !important;
  }

  .ttp-reveal,
  .ttp-reveal--armed {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ── Focus ──────────────────────────────────────────────────
   Never removed. The theme's ring is green on light; on the dark grounds it
   needs to be white to be seen at all. Green KEPT: a focus ring is a functional
   affordance, not decoration, and it is the one place a saturated colour has to
   win against whatever it lands on. */
.ttp-card:focus-visible,
.ttp-btn-ghost:focus-visible {
  outline: 2px solid var(--mv-blue);
  outline-offset: 3px;
}

.ttp-hero a:focus-visible,
.ttp-cta a:focus-visible,
.ttp-nav a:focus-visible,
.ttp-footer a:focus-visible,
.ttp-nav .ttp-btn-ghost:focus-visible {
  outline: 2px solid #FFFFFF;
  outline-offset: 3px;
}

/* A WHITE RING ON A WHITE CARD IS NO RING. The menus hang inside .ttp-nav, so the rule
   directly above reaches every link in them and a keyboard user tabbing through the
   account menu would see nothing at all. Deep green: 5.2:1 on the card, and green on a
   light ground is only ever allowed as a line, never as a letterform. The specificity
   here beats the white rule on its own, whatever order the two end up in. */
.ttp-menu .dropdown-item:focus-visible,
.ttp-alert__action.btn:focus-visible {
  outline: 2px solid var(--mv-cyan);
  outline-offset: 2px;
}

/* Skip link: present on every page, only visible when tabbed to. */
.ttp-skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 1080;
  background: var(--mv-blue);
  color: var(--mv-ink);
  font-weight: 700;
  padding: 0.75rem 1.25rem;
  border-radius: 0 0 var(--ttp-radius) 0;
}

.ttp-skip:focus {
  left: 0;
  outline: 2px solid var(--mv-ink);
  outline-offset: 2px;
}

/* ── Interaction and sequence ────────────────────────────────
   Added last, deliberately: every rule here is an ENHANCEMENT of a page that is
   already complete and legible without it, and the reduced-motion block above
   kills all of it with !important, which beats a later rule that is not.

   GREEN IS STILL THE AI MARK. The breathing glow below is on the AI phase and
   nothing else, which is the one place the brand already spends green in this
   section. A pulse on all five cards would spend the mark on "we have five
   steps", which is not a claim worth a colour. */

/* THE AI PHASE BREATHES.
   Drawn on a pseudo-element rather than by animating the card's own box-shadow,
   for two reasons. Opacity is a compositor property, so this costs no layout and
   no paint on the main thread; and it leaves .ttp-phase:hover free to own the
   card's real box-shadow, so the lift still works while the glow is running.

   The shadow is drawn OUTSIDE the pseudo's border box, so the glow rings the
   card and never touches the text inside it. pointer-events stays none. */
.ttp-phase--ai::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 22px 3px rgba(0, 214, 57, 0.50);
  opacity: 0.18;
  animation: ttp-breathe 4.5s ease-in-out infinite;
}

/* 4.5 seconds, and it never reaches zero. A pulse fast enough to read as a
   heartbeat reads as an alert; this one is slow enough that a reader notices the
   card is alive without being asked to look at it. Holding the floor at 0.18
   rather than 0 stops the glow blinking out of existence at the bottom of every
   cycle, which is the thing that makes a pulse feel cheap. */
@keyframes ttp-breathe {
  0%, 100% { opacity: 0.18; }
  50%      { opacity: 0.85; }
}

/* THE FIVE PHASES ARRIVE IN ORDER.
   They are a sequence, and until now they faded in together, which said the
   opposite. 70ms apart is the measured band where a row reads as dealt rather
   than as staggered-for-the-sake-of-it: below about 40ms the eye sees one event,
   above about 120ms the last card is visibly late.

   This is transition-delay on the existing reveal, not a new mechanism: the
   armed class and .is-visible are portal-home.js's, and a phase that is already
   on screen when the script runs is never armed, so nothing above the fold waits
   for a delay before it can be read. */
.ttp-phases .ttp-phase:nth-child(1).ttp-reveal--armed { transition-delay: 0ms; }
.ttp-phases .ttp-phase:nth-child(2).ttp-reveal--armed { transition-delay: 70ms; }
.ttp-phases .ttp-phase:nth-child(3).ttp-reveal--armed { transition-delay: 140ms; }
.ttp-phases .ttp-phase:nth-child(4).ttp-reveal--armed { transition-delay: 210ms; }
.ttp-phases .ttp-phase:nth-child(5).ttp-reveal--armed { transition-delay: 280ms; }

/* The two engine cards deal the same way, one beat apart. */
.ttp-engines .ttp-engine:nth-child(2).ttp-reveal--armed { transition-delay: 90ms; }

/* THE STREAM ROWS ANSWER THE POINTER.
   Ten rows of source, target and verdict is the densest reading on the page, and
   until now nothing tracked which line the eye was on. The tint is the page's
   own --mv-snow, so the row lights rather than changes colour.

   The negative inline margin and the matching padding are what let the tint run
   past the text to the card's own padding edge: without them the highlight stops
   at the first and last character and reads as a selection, not a row. They are
   declared on the resting state, not on hover, so hovering moves nothing. */
.ttp-engine__tracks .ttp-track {
  margin-inline: -0.7rem;
  padding-inline: 0.7rem;
  border-radius: 9px;
  transition: background-color 170ms var(--ttp-ease);
}

.ttp-engine__tracks .ttp-track:hover { background-color: var(--mv-snow); }

/* A live stream is worth one more degree of attention than one we do not run
   yet: the rows that can actually be bought lift their chip slightly on hover,
   while a coming-soon row tints and does nothing else. */
.ttp-engine__tracks .ttp-track .ttp-chip--live {
  transition: transform 170ms var(--ttp-ease), box-shadow 170ms var(--ttp-ease);
}

.ttp-engine__tracks .ttp-track:hover .ttp-chip--live {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px -2px rgba(0, 214, 57, 0.45);
}

/* ── Premium pass ────────────────────────────────────────────
   GREEN AS TEXT IS STILL FORBIDDEN ON PAPER. Everything below spends green as a
   FILL with charcoal on top, or as text on the charcoal bands where it measures
   8.85:1. Nothing here paints #00D639 onto white. */

/* THE DRY-RUN GUARDRAIL, rebuilt.
   It carried `border-left: 4px solid green`, which is the side-stripe pattern:
   the convention for a log line or a quoted block, not for the one sentence that
   states the product's central safety promise. It also spent the AI mark on a
   border, where a colour cannot be read as a meaning.

   Now it is a raised card like everything else on the page, and the green is
   spent on the shield instead, which is the thing the sentence is about. The
   icon becomes a filled disc with a charcoal glyph, which is the same construction
   as the chips and the AI phase badge, so the page has one way of saying this. */
.ttp-guardrail {
  border-left-width: 1px;
  border-left-color: var(--mv-silver);
  border-top-color: var(--ttp-edge-lit);
  box-shadow: var(--ttp-elev-2);
  align-items: center;
}

.ttp-guardrail .bi {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);        /* 8.85:1 on the green fill */
  font-size: var(--ttp-fs-lg);
  /* The second breathing element on the page, and deliberately in the same
     section as the first: the AI phase proposes, this sentence promises nothing
     is written until the dry run passes. They are one idea, so they share a
     pulse. Offset by half a cycle so they alternate rather than throb together. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
              0 0 0 0 rgba(0, 214, 57, 0);
  animation: ttp-guard-breathe 4.5s ease-in-out -2.25s infinite;
}

@keyframes ttp-guard-breathe {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
                         0 0 6px 0 rgba(0, 214, 57, 0.25); }
  50%      { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
                         0 0 20px 4px rgba(0, 214, 57, 0.60); }
}

/* EVERY SECTION IS MARKED, on instruction.
   An older note in this file argued the opposite: a mark on all five headings
   marks nothing, so only How it works carried a green dot. Hennie asked for
   Support, Pricing and What we migrate to match it, which is a decision about
   what the page should feel like rather than about what the dot denotes, and it
   is his to make. Kept as a green dot with the same halo, so the four section
   openings are one object the way the bullets are.

   What it costs, stated once: the dot is now chrome rather than a mark, so it no
   longer tells a reader that How it works is the section carrying the AI. That
   job now rests entirely on the green card and the guardrail inside it. */
.ttp-eyebrow::before {
  background: var(--mv-blue);
  box-shadow: 0 0 8px 1px rgba(0, 214, 57, 0.65);
}

/* The dark bands keep a quiet dot: a glowing green mark on charcoal competes
   with the hero's own green, and there is no heading there to open. */
.ttp-section--dark .ttp-eyebrow::before {
  background: var(--ttp-text-dark-3);
  box-shadow: none;
}

/* TICK DISCS GET THE SAME MATERIAL AS THE PHASE BADGES.
   They were flat charcoal circles beside cards that now have real elevation, so
   they read as printed on rather than resting on. One highlight, one shadow, the
   same construction as every other small round object on the page. */
.ttp-tick {
  background: linear-gradient(180deg, #3A3A3A 0%, var(--mv-ink) 100%);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.18),
              inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* HERO PROOFS GLOW. Green as text is legal here and only here: on the hero's
   near-black ground #00D639 measures 8.85:1. The drop-shadow gives the three
   automation promises the same halo the AI phase has, so the page's green reads
   as one material in both places. Static, not animated: the hero is the first
   thing a reader meets and it should be still while they read it. */
.ttp-hero__proof .bi {
  filter: drop-shadow(0 0 7px rgba(0, 214, 57, 0.55));
}

/* PANELS LIFT LIKE THE CARDS DO.
   Pricing and Support were the only raised surfaces on the page that did not
   answer the pointer, which made them read as decoration next to the cards
   above them. Same curve, same travel, one elevation step. */
.ttp-panel {
  transition: transform var(--ttp-lift) var(--ttp-ease),
              box-shadow var(--ttp-lift) var(--ttp-ease);
}

.ttp-panel:hover {
  transform: translateY(-2px);
  box-shadow: var(--ttp-elev-4);
}

/* The recessed panel stays recessed. A well that rises on hover is a well that
   was never a well; it deepens instead. */
.ttp-panel--quiet:hover {
  transform: none;
  box-shadow: inset 0 2px 6px rgba(16, 24, 40, 0.09),
              inset 0 0 0 1px rgba(16, 24, 40, 0.03);
}

/* BUTTONS ANSWER THE PRESS.
   Every button on this page lifted on hover and then did nothing at all on the
   way down, so a click had no physical result: the pointer went down and the
   page simply changed. Returning to rest with a tighter shadow is the whole of
   what a press is, and 90ms is short enough to feel like contact rather than
   animation. Anchored on .ttp- ancestors, per this file's namespace rule. */
.ttp-hero__actions .btn-primary,
.ttp-cta .btn-primary,
.ttp-panel .btn-primary,
.ttp-hero__actions .ttp-btn-ghost {
  transition: transform 90ms var(--ttp-ease),
              box-shadow 90ms var(--ttp-ease),
              background-color 170ms var(--ttp-ease);
}

.ttp-hero__actions .btn-primary:active,
.ttp-cta .btn-primary:active,
.ttp-panel .btn-primary:active,
.ttp-hero__actions .ttp-btn-ghost:active {
  transform: translateY(1px) scale(0.985);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.20);
}

/* THE FIVE STEPS FILL THEIR ROWS.
   Three columns and five cards leaves an empty third slot on the second row, and
   a hole at the end of a sequence reads as a missing sixth step rather than as a
   composition. The row above already measured three-across as the widest row
   where a card still carries its sentence, so the answer is not more columns.

   A six-column track with spans is: three cards of two on the first row, two
   cards of three on the second. Both rows are full, no card is narrower than the
   measured floor, and the wider pair on the bottom row reads as the sequence
   arriving rather than as leftovers. Take on and Reconcile also happen to carry
   the longest sentences of the five, so the extra width lands where it is needed.

   At 1440 the shell is about 1080px: a two-span card is 340px, exactly what
   three-across gave before, and a three-span card is 525px. Nothing shrank. */
@media (min-width: 1140px) {
  .ttp-phases { grid-template-columns: repeat(6, minmax(0, 1fr)); }

  .ttp-phases .ttp-phase:nth-child(-n+3) { grid-column: span 2; }
  .ttp-phases .ttp-phase:nth-child(n+4)  { grid-column: span 3; }
}

/* Two across has the same hole one row further down: four cards pair off and the
   fifth sits alone beside a gap. It spans instead, which also gives Reconcile and
   sign off, the longest card of the five, the full measure on a tablet. */
@media (min-width: 620px) and (max-width: 1139.98px) {
  .ttp-phases .ttp-phase:nth-child(5) { grid-column: span 2; }
}

/* ── Colour, and one material for every bullet ────────────────
   Two separate jobs, and they are kept separate on purpose.

   MATERIAL is uniform: every bullet on the page becomes the same object, a
   filled disc with a lit top edge, a shadow under it and a soft halo, which is
   the construction the AI phase badge and the guardrail shield already use. That
   is what makes a page read as designed rather than assembled: one idea about
   what a small round thing IS, applied everywhere.

   COLOUR is not uniform, and must not be. Green still means the machine did it.
   Charcoal is Ted and structure. Violet is Tessa. The page looked plain because
   violet was spending its entire existence on a 4px rule and one chip; the fix
   is to let the engine that owns it actually wear it, not to scatter a fourth
   colour into lists that have no meaning to carry. */

/* Every list glyph becomes a disc. .ttp-list .bi sets colour at this specificity
   already, so the fill and the white glyph have to be declared together here or
   that rule paints charcoal on charcoal, which is the exact bug the comment on
   .ttp-tick's glyph records. */
.ttp-list .bi,
.ttp-contact .bi {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.55rem;
  height: 1.55rem;
  margin-top: 0.05rem;
  border-radius: 50%;
  background: linear-gradient(180deg, #3A3A3A 0%, var(--mv-ink) 100%);
  color: #FFFFFF;
  font-size: var(--ttp-fs-xs);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.20),
              0 4px 10px -3px rgba(16, 24, 40, 0.28),
              inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* The free-list tick grows to match its neighbours and gains the same halo. It
   stays CHARCOAL: the recorded reason still holds, "this part is free" is a
   commercial fact and not the AI, and re-greening it would spend the mark twice. */
.ttp-tick {
  width: 1.55rem;
  height: 1.55rem;
  margin-top: 0.05rem;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.20),
              0 4px 10px -3px rgba(16, 24, 40, 0.28),
              inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

.ttp-list li,
.ttp-contact li { align-items: flex-start; }

/* ── Tessa wears her own colour ───────────────────────────────
   Violet is already the platform's second identity colour and it was almost
   invisible. Now the payroll card's surface, its rows, its closing line and its
   hover all carry it, so a reader can tell the two engines apart from across a
   room rather than by reading a 4px rule. Ted keeps charcoal, which is his. */
.ttp-engine--payroll {
  border-top-color: #D9CDFB;
}

/* A violet wash at the top of the card, fading out well before the text. Six
   percent at the top edge and nothing by a third of the way down: enough to tint
   the surface, not enough to touch any contrast measurement on the card. */
.ttp-engine--payroll::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 38%;
  background: linear-gradient(180deg, rgba(122, 90, 248, 0.07), rgba(122, 90, 248, 0));
  pointer-events: none;
}

.ttp-engine--payroll .ttp-engine__tracks .ttp-track:hover {
  background-color: var(--ttp-payroll-tint);
}

.ttp-engine--payroll .ttp-engine__meta {
  color: var(--ttp-payroll-deep);   /* 6.81:1 on white */
  font-weight: 600;
}

/* Ted's own closing line gets the same weight so the pair stays symmetrical:
   the difference between the two cards should be the colour, not the emphasis. */
.ttp-engine:not(.ttp-engine--payroll) .ttp-engine__meta {
  color: var(--mv-graphite);        /* 10.31:1 on white */
  font-weight: 600;
}

/* The card contents sit above the wash. */
.ttp-engine__head,
.ttp-engine__blurb,
.ttp-engine__tracks,
.ttp-engine__meta { position: relative; }

/* ── Panel titles carry an icon ───────────────────────────────
   Four panels across Pricing and Support all opened with the same weight of
   heading and nothing to tell them apart. Each now opens with its own icon, on
   the same green disc every bullet and badge on the page already uses, so a
   reader can tell the panels apart before reading a word.

   This replaces a 3px rule that used to sit here. The rule was structure with no
   meaning: it said "a heading starts" on four headings that were already
   obviously headings. An icon says which panel. */
.ttp-panel__title {
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

.ttp-panel__title .bi {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.85rem;
  height: 1.85rem;
  border-radius: 50%;
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);          /* 8.85:1 on the green fill */
  font-size: var(--ttp-fs-sm);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.16),
              0 5px 14px -4px rgba(0, 214, 57, 0.55),
              inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

/* ── The free-tier panel is inverted ──────────────────────────
   Pricing was four white panels in a white-and-snow page, and the panel that
   carries the commercial promise looked exactly like the one carrying the small
   print beside it. Colour was not the missing thing; CONTRAST was. Dropping this
   one panel to charcoal gives the section an anchor, gives the page a dark block
   between two light bands, and costs no new hue at all: it is the ground the
   navbar, hero, closing band and footer already use.

   It also settles an old tension. The tick discs here were charcoal on white,
   recorded as "this part is free is a commercial fact and not the AI", which is
   right and still holds. On a charcoal ground they invert to white discs with a
   charcoal glyph: the same fill-plus-contrasting-glyph construction as every
   other disc on the page, still not spending the green mark. */
.ttp-pricing .ttp-panel:first-child {
  background: linear-gradient(170deg, var(--mv-ink) 0%, var(--ttp-night) 100%);
  border-color: #000000;
  border-top-color: rgba(255, 255, 255, 0.10);
  box-shadow: var(--ttp-elev-4);
}

.ttp-pricing .ttp-panel:first-child .ttp-wordmark { color: #FFFFFF; }

.ttp-pricing .ttp-panel:first-child .ttp-panel__title {
  color: #FFFFFF;                          /* 17.40:1 */
}

/* On the charcoal panel the icon keeps its green fill and gains a stronger halo,
   which is earned here: green on charcoal measures 8.85:1, and this panel is the
   single thing on the page the whole product is built to make true. */
.ttp-pricing .ttp-panel:first-child .ttp-panel__title .bi {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45),
              0 6px 18px -4px rgba(0, 214, 57, 0.65),
              inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

.ttp-pricing .ttp-panel:first-child .ttp-list li {
  color: var(--ttp-text-dark-1);           /* 14.87:1 */
  border-bottom-color: var(--ttp-hair-dark);
}

.ttp-pricing .ttp-panel:first-child .ttp-note {
  color: var(--ttp-text-dark-2);           /* 7.32:1 */
}

/* Inverted disc: white fill, charcoal glyph. */
.ttp-pricing .ttp-panel:first-child .ttp-tick {
  background: linear-gradient(180deg, #FFFFFF 0%, #E8EAED 100%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45),
              0 4px 12px -3px rgba(0, 0, 0, 0.55),
              inset 0 1px 0 rgba(255, 255, 255, 0.9);
}

.ttp-pricing .ttp-panel:first-child .ttp-tick .bi { color: var(--mv-ink); }

/* The panel is already the section's anchor; it does not also need to jump. */
.ttp-pricing .ttp-panel:first-child:hover {
  transform: translateY(-2px);
  box-shadow: var(--ttp-elev-5);
}

/* ── One tinted section ───────────────────────────────────────
   How it works is the only section that carries both green elements, so it is
   the only one that gets a ground of its own: a green tint far below the
   threshold where it reads as a colour, and just above the one where the section
   reads as the same white as the two around it. Measured at 0.6% saturation, so
   no text on it moves by a hundredth of a contrast point. */
.ttp-section--paper {
  background: linear-gradient(180deg, #FBFEFC 0%, #F7FCF9 100%);
}

/* The snow sections take the same tint one step down, so the page alternates
   between two greens-of-almost-nothing rather than between white and grey.
   Both are far below the threshold where a reader would call either a colour,
   and no text on either moves by a hundredth of a contrast point. */
.ttp-section--light {
  background: linear-gradient(180deg, #F7FBF8 0%, #F3F9F5 100%);
}

/* ── Bullets: one green disc everywhere ───────────────────────
   TWO fixes in one block.

   1. THE NESTED DISC BUG. The rule above was written as `.ttp-list .bi`, a
      descendant selector, so it also matched the glyph INSIDE .ttp-tick and
      built a second disc within the first. On the inverted pricing panel that
      read as a dark blob sitting on top of a white circle. Every disc rule below
      uses the CHILD combinator, `> li >`, so a glyph nested inside a tick is a
      glyph and nothing else.

   2. GREEN, ON HENNIE'S INSTRUCTION. The note on .ttp-tick records the earlier
      decision to keep these charcoal, on the reasoning that "this part is free"
      is a commercial fact and not the AI. That reasoning was put to him twice and
      overruled twice: the bullets across Pricing and Support are to match the
      How it works marks. Recorded here rather than deleted up there, so the next
      reader sees that it was a decision and not a drift.

   Contrast is unaffected: the glyph is charcoal on the green fill at 8.85:1,
   which is the same fill-plus-charcoal construction the chips, the AI phase
   badge and the guardrail shield already use, on light grounds and dark alike. */
.ttp-list > li > .bi,
.ttp-contact > li > .bi,
.ttp-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.55rem;
  height: 1.55rem;
  margin-top: 0.05rem;
  border-radius: 50%;
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);          /* 8.85:1 on the green fill */
  font-size: var(--ttp-fs-xs);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.16),
              0 5px 14px -4px rgba(0, 214, 57, 0.55),
              inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

/* The tick's inner glyph is a GLYPH. It had become a disc inside a disc. */
.ttp-tick > .bi {
  display: inline;
  width: auto;
  height: auto;
  margin: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--mv-ink);          /* 8.85:1 on the green fill */
  font-size: var(--ttp-fs-xs);
}

/* On the inverted panel the disc keeps the same green and the same charcoal
   glyph: green on charcoal measures 8.85:1, so nothing has to change to sit on
   the dark ground, which is the point of a fill that carries its own contrast. */
.ttp-pricing .ttp-panel:first-child .ttp-tick {
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45),
              0 6px 18px -4px rgba(0, 214, 57, 0.60),
              inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

.ttp-pricing .ttp-panel:first-child .ttp-tick > .bi { color: var(--mv-ink); }

/* The hero's three proofs join the same family. They were green glyphs with a
   halo, which was the only place on the page a bullet was a coloured letterform
   rather than a fill, so they were the last thing breaking the rule that a
   bullet is one object. Green fill, charcoal glyph, 8.85:1 both ways: the disc
   needs no change to sit on the charcoal ground, which is the whole advantage of
   a mark that carries its own contrast. Slightly smaller than the panel discs,
   because these sit on a line of text rather than at the head of a paragraph. */
.ttp-hero__proof .bi {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 50%;
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);
  font-size: 0.7rem;
  filter: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35),
              0 5px 16px -4px rgba(0, 214, 57, 0.65),
              inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

/* ── The AI phase is a green card ─────────────────────────────
   THREE VERSIONS SHIPPED IN ONE DAY, and the third is the one that was always
   available. Written out because the sequence is the lesson.

   1. Brand green with WHITE text, on instruction after the contrast was put to
      Hennie: "do it even if banned". White on #00D639 is 1.97:1. That is the
      same pair this file's header bans, inverted: green type on white and white
      type on green are ONE measurement. He could not read it.
   2. The green walked down its own hue to #007D21 so white would pass at 5.31:1.
      Readable, and too dark: it stopped looking like the brand.
   3. THIS ONE. Brand green kept exactly as it is, and the TEXT turned charcoal.
      #1A1A1A on #00D639 is 8.85:1, nearly twice what version 2 bought, on the
      bright card he wanted in the first place.

   The trap in versions 1 and 2 was treating the background as the variable. On a
   green surface the readable pair was never white, it was charcoal, and it is
   the construction every chip, badge and disc on this page already uses.

   4. FINAL. The fill was dropped entirely: WHITE CARD, BLACK TEXT, THICK GREEN
      BORDER, green badge and green tag. The card is marked by its edge instead
      of its surface, which is the version that survives every reading of it.

   Why this one ends the sequence. A green fill forces every element on the card
   to be chosen against green, and each choice loses something: white type fails
   at 1.97:1, charcoal type passes but flattens the badge and the tag into the
   same value as the body, and the green tag disappears into its own background.
   A border carries the same "this is the AI step" signal, costs nothing in
   contrast, and leaves the badge, the tag and the glow free to stay green and
   actually read as green. The card sits on white like its four siblings, so the
   row still scans as one sequence rather than as four cards and a poster. */
.ttp-phase--ai {
  background: var(--mv-white);
  /* 2px, and the whole border rather than a top rule. box-sizing is border-box
     application-wide, so the thicker edge eats into the padding and the card
     does not change size or shift the two beside it. Two pixels is the point
     where the edge still reads as deliberate at arm's length and stops reading
     as a box drawn around the card. */
  border: 2px solid var(--mv-blue);
  box-shadow: var(--ttp-elev-2);
}

/* NO type overrides. The heading and the body inherit .ttp-phase, so this card
   reads at exactly the same weight as its four siblings and the only things
   that differ are its edge and its three green marks. Overriding the body to
   black made it the heaviest paragraph in the row, which pulled the eye for the
   wrong reason: the card is the AI step, not the important step. */

/* The top rule is gone: the border now does that job on all four sides, and the
   two together read as a doubled edge. */
.ttp-phase--ai::before { display: none; }

/* The badge goes back to green on a white card, where it is legible again and
   is one of the three green marks the card now carries: border, badge, tag. */
.ttp-phase--ai .ttp-phase__num {
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);          /* 8.85:1 on the green fill */
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.12),
              0 6px 16px -4px rgba(0, 214, 57, 0.45),
              inset 0 1px 0 rgba(255, 255, 255, 0.55);
}

/* GREEN AGAIN, which it could not be while the card was green. Fill with
   charcoal on top, the platform's one construction for a green mark. */
.ttp-phase__tag {
  background: linear-gradient(180deg, #2BEE5C 0%, var(--mv-blue) 100%);
  color: var(--mv-ink);          /* 8.85:1 on the green fill */
  box-shadow: 0 2px 10px -2px rgba(0, 214, 57, 0.55);
}

/* The breathing halo reads again now that the card behind it is white. */
.ttp-phase--ai::after {
  box-shadow: 0 0 24px 3px rgba(0, 214, 57, 0.55);
}

/* ── Support gets a hero ──────────────────────────────────────
   It was the flattest section on the page: two identical white panels, no
   anchor, nothing to look at first. Every other section had one thing carrying
   it, How it works the green AI card and Pricing the inverted charcoal panel,
   and Support had nothing.

   "What we support" takes the AI card's exact treatment, which is what makes the
   two sections rhyme: brand green, charcoal type at 8.85:1, and the same
   direction rule, so lightening this card is safe and darkening it is not. The
   list beside it stays white and recessed, which is the same raised-against-
   recessed pair Pricing already uses. */
/* Marked by its EDGE, exactly as the AI step is. The green fill this carried
   first had the same problem step two had: every element on it then has to be
   chosen against green, and the bullets had to invert to charcoal to survive,
   which made them the one set on the page that did not match the other twelve.
   White card, green border, and everything inside it goes back to the defaults. */
.ttp-support .ttp-panel:first-child {
  background: var(--mv-white);
  border: 2px solid var(--mv-blue);
  box-shadow: var(--ttp-elev-2);
}

.ttp-support .ttp-panel:first-child:hover {
  transform: translateY(-2px);
  box-shadow: var(--ttp-elev-5);
}
