/* =====================================================================
   Tarvi Living Widget — Section 2 companion panel.
   Self-contained: does not touch aivons.css, admentus-brand.css, or any
   other template file. Palette values match _shared/visual-identity.md
   directly (no dependency on admentus-brand.css's CSS variables), so this
   file can be reviewed and rolled back in isolation.
   ===================================================================== */

/* Default / no-JS / pre-mount state: the panel slot stays hidden and the prose
   above it is all that shows. widget.js removes .tarvi-pending from the #tarvi
   section once mountPanel() has built and wired the panel — the ONLY thing that
   reveals it. If the scripts never run, this is what stays on the page: prose
   only, not a broken widget.

   Hides the SLOT, never the .row containing it. widget.js triggers mount from an
   IntersectionObserver on that row, and a display:none element generates no box
   and can never intersect — hiding the row would deadlock the widget into never
   mounting at all.

   (Previously `.welcome-one .row.tarvi-pending > [class*="col-"]:last-child`,
   which matched nothing: the class sits on the section rather than a .row, and
   the row's only child carries no col-* class. The guard never once fired.) */
.tarvi-pending #tarvi-panel-slot {
    display: none;
}

/* aivons-responsive.css adds margin-top to .welcome-one__right on
   tablet/mobile to separate it from the image that used to sit above it
   when it was always the second/right column. The Section 2 column swap
   made .welcome-one__right the FIRST column, so that margin now creates
   dead space above "Hi — I'm Tarvi." with nothing above it to separate
   from. Zero it out when .welcome-one__right is the first column.
   Scoped to #tarvi specifically (not just .welcome-one) — a second,
   unrelated .welcome-one section ("About Jeff") already has
   .welcome-one__right as its first column by original template design;
   this fix must not touch that section's layout. */
#tarvi .row > [class*="col-"]:first-child > .welcome-one__right {
    margin-top: 0;
}

/* Flat like Option C, not a bordered card: the panel's own fill IS the
   darkest navy (var(--thm-dark-blue)) with no border. That darkness is what lets the
   chip/input/bubble outlines (var(--thm-royal-blue);) and fills (#0a2472, var(--thm-cobalt-blue);) read as
   distinct steps instead of blending into a lighter, bordered surface — a
   lighter #052777 card was tried and rejected for exactly that reason.
   .tarvi-panel-wrap (index.html) is a full-bleed .container-fluid outside
   the prose's normal .container, specifically so this 80% has real screen
   width to work with instead of a ~680px column. */
.tarvi-panel {
    background: var(--thm-dark-blue);
    border-radius: 12px;
    padding: 32px 28px;
    max-width: 80%;
    margin: 0 auto;
    font-family: var(--thm-typography-text-font-family), Sans-serif;
    box-shadow: 0px 8px 8px 0px rgba(0, 0, 0, 0.4);
}

/* Tarvi mark — live-state indicator (waiting/thinking/talking), set via
   widget.js's setMarkState(). Markup: mark.js.

   The portrait raster ships with its rim DE-LIT (see build-tarvi-portrait.py),
   so every photon here comes from the vector rim below. That is the point of
   the rebuild: a raster can't animate, a traced contour can.

   --rim-w scales stroke weight with display size. At 96px the traced 3-unit
   core reads fine; at the 48px launcher (36px mobile) a 648-unit viewBox puts
   that under half a CSS pixel, so it gets boosted rather than vanishing. */
.tarvi-mark {
    --rim-w: 1;
    isolation: isolate;
    display: block;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    flex-shrink: 0;
}

.tarvi-mark .tmark-rim,
.tarvi-mark .tmark-arc-halo,
.tarvi-mark .tmark-arc-glow,
.tarvi-mark .tmark-arc-core,
.tarvi-mark .face-wash { mix-blend-mode: screen; }

.tarvi-mark .rim-core { stroke-width: calc(2.9 * var(--rim-w)); }
.tarvi-mark .rim-glow { stroke-width: calc(7.7 * var(--rim-w)); }
.tarvi-mark .rim-bloom { stroke-width: calc(14 * var(--rim-w)); }

/* Comet geometry. pathLength is normalised to 1000 on the shared rim path, so
   these are percentages of the perimeter regardless of its real length.

   All 14 segments read one inherited --arc-shift (registered below so it can be
   animated as a real number), which keeps the train rigid while it travels --
   animating each segment's own dashoffset would let them drift apart. Segment 0
   is the head; each subsequent one sits 12 further back at a lower opacity. */
@property --arc-shift {
    syntax: '<number>';
    initial-value: 0;
    inherits: true;
}

.tarvi-mark .arc-seg {
    stroke-dasharray: 15 985;
    opacity: 0;
}
.tarvi-mark .tmark-arc-halo .arc-seg { stroke-width: calc(20 * var(--rim-w)); }
.tarvi-mark .tmark-arc-glow .arc-seg { stroke-width: calc(12 * var(--rim-w)); }
.tarvi-mark .tmark-arc-core .arc-seg { stroke-width: calc(4.2 * var(--rim-w)); }

.tarvi-mark .arc-seg-0 { stroke-dashoffset: calc((var(--arc-shift) + 0) * 1px); --seg-op: 1.0; }
.tarvi-mark .arc-seg-1 { stroke-dashoffset: calc((var(--arc-shift) + 12) * 1px); --seg-op: 0.898; }
.tarvi-mark .arc-seg-2 { stroke-dashoffset: calc((var(--arc-shift) + 24) * 1px); --seg-op: 0.798; }
.tarvi-mark .arc-seg-3 { stroke-dashoffset: calc((var(--arc-shift) + 36) * 1px); --seg-op: 0.702; }
.tarvi-mark .arc-seg-4 { stroke-dashoffset: calc((var(--arc-shift) + 48) * 1px); --seg-op: 0.609; }
.tarvi-mark .arc-seg-5 { stroke-dashoffset: calc((var(--arc-shift) + 60) * 1px); --seg-op: 0.519; }
.tarvi-mark .arc-seg-6 { stroke-dashoffset: calc((var(--arc-shift) + 72) * 1px); --seg-op: 0.434; }
.tarvi-mark .arc-seg-7 { stroke-dashoffset: calc((var(--arc-shift) + 84) * 1px); --seg-op: 0.352; }
.tarvi-mark .arc-seg-8 { stroke-dashoffset: calc((var(--arc-shift) + 96) * 1px); --seg-op: 0.275; }
.tarvi-mark .arc-seg-9 { stroke-dashoffset: calc((var(--arc-shift) + 108) * 1px); --seg-op: 0.204; }
.tarvi-mark .arc-seg-10 { stroke-dashoffset: calc((var(--arc-shift) + 120) * 1px); --seg-op: 0.138; }
.tarvi-mark .arc-seg-11 { stroke-dashoffset: calc((var(--arc-shift) + 132) * 1px); --seg-op: 0.08; }
.tarvi-mark .arc-seg-12 { stroke-dashoffset: calc((var(--arc-shift) + 144) * 1px); --seg-op: 0.031; }
.tarvi-mark .arc-seg-13 { stroke-dashoffset: calc((var(--arc-shift) + 156) * 1px); --seg-op: 0.0; }

@keyframes tarvi-arc-sweep {
    from { --arc-shift: 0; }
    to { --arc-shift: -1000; }
}
@keyframes tarvi-rim-breathe {
    0%, 100% { opacity: var(--breathe-lo); }
    50% { opacity: var(--breathe-hi); }
}

/* The three states have to be legible at 48px and at a glance, so they differ
   on BOTH axes: total light output, and whether that light is static, moving,
   or breathing. Rim intensity alone was too subtle a cue — the faceplate wash
   is what makes the jump between them read. */

.tarvi-mark .face-wash { opacity: 0; }

/* Waiting — faceplate near-black, faint static perimeter at ~25%. No motion. */
.tarvi-mark[data-state="waiting"] .rim-core { opacity: 0.26; }
.tarvi-mark[data-state="waiting"] .rim-glow { opacity: 0.13; }
.tarvi-mark[data-state="waiting"] .rim-bloom { opacity: 0.07; }
.tarvi-mark[data-state="waiting"] .face-wash { opacity: 0.04; }

/* Thinking — perimeter stays dim, one bright arc travels it. The arc runs well
   above the resting rim so the moving light is the thing you notice; 3.6s per
   lap is quick enough to catch the eye without looking agitated. */
.tarvi-mark[data-state="thinking"] .rim-core { opacity: 0.18; }
.tarvi-mark[data-state="thinking"] .rim-glow { opacity: 0.11; }
.tarvi-mark[data-state="thinking"] .rim-bloom { opacity: 0.06; }
.tarvi-mark[data-state="thinking"] .face-wash { opacity: 0.04; }
.tarvi-mark[data-state="thinking"] .tmark-arc-halo,
.tarvi-mark[data-state="thinking"] .tmark-arc-glow,
.tarvi-mark[data-state="thinking"] .tmark-arc-core {
    animation: tarvi-arc-sweep 3.6s linear infinite;
}
.tarvi-mark[data-state="thinking"] .tmark-arc-halo .arc-seg { opacity: calc(var(--seg-op) * 0.4); }
.tarvi-mark[data-state="thinking"] .tmark-arc-glow .arc-seg { opacity: calc(var(--seg-op) * 0.9); }
.tarvi-mark[data-state="thinking"] .tmark-arc-core .arc-seg { opacity: var(--seg-op); }

/* Talking — whole perimeter lit and the faceplate washes with it, gently
   modulating while text streams. The wash swings hardest (0.10 → 0.30) because
   an area change reads from further away than an edge change. */
.tarvi-mark[data-state="talking"] .rim-core {
    --breathe-lo: 0.6; --breathe-hi: 1;
    opacity: 1;
    animation: tarvi-rim-breathe 1.9s ease-in-out infinite;
}
.tarvi-mark[data-state="talking"] .rim-glow {
    --breathe-lo: 0.45; --breathe-hi: 0.9;
    opacity: 0.9;
    animation: tarvi-rim-breathe 1.9s ease-in-out infinite;
}
.tarvi-mark[data-state="talking"] .rim-bloom {
    --breathe-lo: 0.28; --breathe-hi: 0.62;
    opacity: 0.62;
    animation: tarvi-rim-breathe 1.9s ease-in-out infinite;
}
.tarvi-mark[data-state="talking"] .face-wash {
    --breathe-lo: 0.10; --breathe-hi: 0.30;
    opacity: 0.30;
    animation: tarvi-rim-breathe 1.9s ease-in-out infinite;
}

/* Motion off: hold each state at a distinguishable static brightness. The
   thinking arc parks at its start position rather than disappearing, so all
   three states stay tellable apart without animation. */
@media (prefers-reduced-motion: reduce) {
    .tarvi-mark .rim-core,
    .tarvi-mark .rim-glow,
    .tarvi-mark .rim-bloom,
    .tarvi-mark .tmark-arc-halo,
    .tarvi-mark .tmark-arc-glow,
    .tarvi-mark .tmark-arc-core,
    .tarvi-mark .face-wash {
        animation: none !important;
    }
}

/* Idle state — mark + headline + subtitle, centered. Shown at rest; this
   IS the "here now" moment, no separate transcript needed until the
   visitor actually engages (Option C: quiet until engaged). */
.tarvi-panel__idle {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 6px;
    margin-bottom: 22px;
    overflow: hidden;
    transition: opacity 0.2s ease, max-height 0.25s ease, margin 0.25s ease;
}

.tarvi-panel__idle.is-collapsed {
    opacity: 0;
    max-height: 0;
    margin: 0;
    pointer-events: none;
}

.tarvi-panel__idle .tarvi-mark {
    margin-bottom: 8px;
}

/* Same accent treatment as .welcome-one__text-accent (26px/500 Roboto
   Condensed, matched to the live site) — just Ghost White instead of
   Admentus Blue, since blue-on-navy would disappear on this dark panel. */
.tarvi-panel__idle-name {
    font-family: 'Roboto Condensed', sans-serif;
    font-size: 26px;
    font-weight: 500;
    line-height: 1.2;
    color: var(--thm-ghost-white);
}

/* Body copy sized to match the site's 15px/400 Roboto baseline. Explicit
   weight:400 needed — the template's body{} default is 500. */
.tarvi-panel__idle-sub {
    font-size: 15px;
    font-weight: 400;
    line-height: 1.6;
    color: #a1afe1;
    max-width: 440px;
}

/* Active state — compact header replaces the idle headline once the
   visitor sends a first message. Hidden until then. */
.tarvi-panel__active-head {
    display: none;
    align-items: center;
    gap: 14px;
    margin-bottom: 18px;
}

.tarvi-panel__active-head.is-shown {
    display: flex;
}

.tarvi-panel__identity {
    display: flex;
    flex-direction: column;
}

.tarvi-panel__name {
    display: block;
    font-family: var(--thm-typography-text-font-family);
    font-size: 20px;
    font-weight: 500;
    line-height: 1.2;
    text-transform: uppercase;
    color: var(--thm-ghost-white);
}

.tarvi-panel__status {
    display: block;
    font-size: 11px;
    line-height: 1.5;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--thm-royal-blue);
}

/* Transcript — collapsed (zero height) at rest; expands to a bounded,
   internally-scrolling area once the conversation starts. This fixed
   max-height is what guarantees a real back-and-forth only ever scrolls
   inside the panel — it can never grow the page layout. */
.tarvi-panel__transcript {
    max-height: 0;
    overflow-y: auto;
    margin-bottom: 0;
    padding-right: 4px;
    transition: max-height 0.3s ease, margin-bottom 0.3s ease;
}

.tarvi-panel__transcript.is-shown {
    max-height: 360px;
    margin-bottom: 18px;
}

.tarvi-panel__transcript::-webkit-scrollbar {
    width: 5px;
}

.tarvi-panel__transcript::-webkit-scrollbar-thumb {
    background: var(--thm-royal-blue);
    border-radius: 4px;
}

.tarvi-panel__bubble {
    border-radius: 10px;
    padding: 10px 14px;
    margin-bottom: 10px;
    font-size: 15px;
    font-weight: 400;
    line-height: 1.6;
    max-width: 85%;
}

/* Outlined, no fill — same contrast mechanism as the chips and input
   border. A translucent fill-on-fill tint here was tried and rejected:
   with no edge to define it, it read as flat and hard to distinguish from
   the panel background. */
.tarvi-panel__bubble--tarvi {
    background: transparent;
    border: 1px solid var(--thm-royal-blue);
    color: var(--thm-ghost-white);
}

/* A genuinely different lightness step (solid Cobalt), not a tint — that's
   what makes a filled surface actually read against the dark panel. */
.tarvi-panel__bubble--user {
    background: var(--thm-cobalt-blue);
    color: var(--thm-ghost-white);
    margin-left: auto;
}

/* Markdown inside a Tarvi bubble (rendered by markdown.js). The endpoint
   returns **bold**, "- " bullets and \n\n paragraphs; before this they rendered
   as one undifferentiated wall of prose with literal asterisks. The bubble is a
   div rather than a p precisely so these block elements can live inside it. */
.tarvi-panel__bubble p,
.tarvi-panel__bubble .tarvi-md-list {
    margin: 0 0 10px;
}

/* aivons.css styles bare <p> globally (grey #828282, 30px line-height). Before
   markdown rendering the bubble WAS the <p> and carried its own colour, so the
   template rule never applied; now that replies contain nested paragraphs it
   does, and it turned reply text grey and set paragraphs looser than the list
   items beside them. Inherit from the bubble instead of restating values, so
   this stays correct if the bubble's own type changes. */
.tarvi-panel__bubble p {
    color: inherit;
    font-family: inherit;
    font-size: inherit;
    font-weight: inherit;
    line-height: inherit;
}

.tarvi-panel__bubble p:last-child,
.tarvi-panel__bubble .tarvi-md-list:last-child {
    margin-bottom: 0;
}

.tarvi-panel__bubble .tarvi-md-list {
    padding-left: 20px;
}

.tarvi-panel__bubble .tarvi-md-list li {
    margin-bottom: 6px;
}

.tarvi-panel__bubble .tarvi-md-list li:last-child {
    margin-bottom: 0;
}

/* Ghost White against the bubble's #a1afe1-adjacent body copy — enough of a
   step to mark emphasis without introducing a second accent colour. */
.tarvi-panel__bubble strong {
    font-weight: 600;
    color: #fff;
}

.tarvi-panel__bubble em {
    font-style: italic;
}

.tarvi-panel__bubble code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.9em;
    background: rgba(90, 116, 202, 0.22);
    border-radius: 4px;
    padding: 1px 5px;
}

.tarvi-panel__bubble a {
    color: var(--thm-ghost-white);
    text-decoration: underline;
}

.tarvi-panel__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
    margin-bottom: 18px;
    overflow: hidden;
    transition: opacity 0.2s ease, max-height 0.25s ease, margin 0.25s ease;
}

.tarvi-panel__chips.is-collapsed {
    opacity: 0;
    max-height: 0;
    margin: 0;
    pointer-events: none;
}

.tarvi-panel__chip {
    background: transparent;
    border: 1px solid var(--thm-royal-blue);
    border-radius: 20px;
    padding: 8px 16px;
    font-family: var(--thm-typography-text-font-family), Sans-serif;
    font-size: var(--thm-typography-text-font-size);
    color: var(--thm-ghost-white);
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.tarvi-panel__chip:hover,
.tarvi-panel__chip:focus-visible {
    background-color: rgba(90, 116, 202, 0.2);
}

/* Cloudflare Turnstile challenge — mounted inside the panel, directly above
   the input, so an interactive "Verify you are human" checkbox lands where the
   visitor is already looking. It previously rendered into a slot outside the
   panel (Home) or appended to document.body beneath the modal overlay
   (sub-pages), where it was unnoticeable or unclickable respectively.

   This block is always present and never display:none — Turnstile cannot
   initialize into a hidden container, and appearance:'interaction-only' keeps
   it at zero height until Cloudflare actually asks for a click. Only the note
   and the surrounding margin are toggled (widget.js/modal.js). */
.tarvi-panel__challenge {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

.tarvi-panel__challenge.is-active {
    margin-bottom: 18px;
}

.tarvi-panel__challenge-note {
    font-size: 15px;
    font-weight: 400;
    line-height: 1.6;
    color: var(--thm-ghost-white);
    text-align: center;
}

.tarvi-panel__form {
    display: flex;
    align-items: center;
    gap: 8px;
    border: 1px solid var(--thm-royal-blue);
    border-radius: 24px;
    padding: 8px 8px 8px 18px;
    background: var(--thm-cobalt-blue);
}

.tarvi-panel__input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    font-family: var(--thm-typography-text-font-family), Sans-serif;
    font-size: var(--thm-typography-text-font-size);
    color: var(--thm-ghost-white);
}

.tarvi-panel__input::placeholder {
    color: #fff;
}

.tarvi-panel__submit {
    background: var(--thm-royal-blue);
    color: #fff;
    border: none;
    border-radius: 50%;
    width: 32px;
    height: 32px;
    font-size: 18px;
    font-weight: 800;
    cursor: pointer;
    flex-shrink: 0;
    line-height: 1
}

.tarvi-panel__submit:hover,
.tarvi-panel__submit:focus-visible {
    background: var(--thm-admentus-blue);
}

/* ---------------------------------------------------------------------------
   TARVI LAUNCHER + MODAL — sub-page entry point. Home keeps its full inline
   #tarvi section (widget.js); sub-pages get a floating launcher (modal.js)
   that opens this same .tarvi-panel component as an overlay instead. The
   launcher occupies the bottom-right corner that .scroll-to-top would
   otherwise use -- admentus-brand.css hides that button on sub-pages
   (body.page--no-hero) so the two never compete for the same spot.
   --------------------------------------------------------------------------- */

.tarvi-launcher {
    position: fixed;
    right: 24px;
    bottom: 24px;
    width: 64px;
    height: 64px;
    border-radius: 50%;
    border: none;
    padding: 0;
    background: var(--thm-dark-blue);
    box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.45);
    cursor: pointer;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 200ms ease;
}

.tarvi-launcher:hover,
.tarvi-launcher:focus-visible {
    transform: scale(1.06);
}

.tarvi-launcher .tarvi-mark {
    width: 48px;
    height: 48px;
    /* 648-unit viewBox shown at 48px: boost the rim so it survives the
       downscale instead of aliasing away. */
    --rim-w: 2.1;
}

.tarvi-modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(3, 8, 20, 0.72);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    z-index: 1000;
    opacity: 0;
    visibility: hidden;
    transition: opacity 200ms ease;
}

.tarvi-modal-overlay.is-open {
    opacity: 1;
    visibility: visible;
}

/* Width matches the Home inline panel (which renders at 80% of the ~1170px
   content row, i.e. ~936px) so the two surfaces read as the same component
   rather than two different widgets. It was 480px, which turned any long reply
   into a towering narrow column — the lists and paragraphs the markdown
   renderer produces need real horizontal room to be scannable.
   Narrower viewports still shrink it: width:100% plus the overlay's 24px
   padding means the cap only applies once there's space for it. */
.tarvi-modal-dialog {
    position: relative;
    width: 100%;
    max-width: 936px;
    transform: translateY(12px);
    transition: transform 200ms ease;
}

.tarvi-modal-overlay.is-open .tarvi-modal-dialog {
    transform: translateY(0);
}

/* Scrolling lives on the panel itself, not the dialog wrapper -- keeping
   the wrapper's overflow visible (the default) is what lets .tarvi-modal-close
   hang outside its top-right corner without being clipped. Setting
   overflow-y alone on the wrapper would also force overflow-x to auto per
   the CSS spec, which clipped the close button AND added a stray horizontal
   scrollbar -- both fixed by moving the scroll boundary down to the panel. */
.tarvi-modal-dialog .tarvi-panel {
    max-width: 100%;
    max-height: 88vh;
    overflow-y: auto;
}

.tarvi-modal-close {
    position: absolute;
    top: -16px;
    right: -16px;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: none;
    background: var(--thm-cobalt-blue);
    color: #fff;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    z-index: 1;
}

body.tarvi-modal-locked {
    overflow: hidden;
}

@media (max-width: 767px) {
    .tarvi-launcher {
        width: 52px;
        height: 52px;
        right: 16px;
        bottom: 16px;
    }

    .tarvi-launcher .tarvi-mark {
        width: 36px;
        height: 36px;
        --rim-w: 2.8;
    }

    .tarvi-modal-dialog {
        max-height: 92vh;
    }
}
