/* ============================================================================
   Tool studio — the shared skin for the formatter tools
   ----------------------------------------------------------------------------
   Opt-in: a page wears this look by putting .tstudio on its shell. The 18 tool pages share
   assets/css/jch-tools.css for the .jx-* split-pane machinery, so restyling those classes
   globally would redesign every tool at once. This sheet loads after it and only bites inside
   .tstudio, so pages adopt the skin one at a time.

   Worn by: every tools-*.html page, plus compiler-simple.html and compiler-spring.html
            (those two take the buttons and pane dots only - see the note in ide-skin).
   Originally: tools-sql.html, tools-json.html, tools-yaml-json.html,
            tools-properties-yaml.html, tools-base64.html, tools-jwt.html,
            tools-regex.html, tools-uuid.html.
   ========================================================================= */

/* .ts-tokens carries the palette WITHOUT the skin.
   ----------------------------------------------------------------------------
   Every one of these pages parks its Bootstrap modals after </main>, as a direct
   child of <body> - which is outside .tstudio, so none of the custom properties
   below reached them. A .ts-btn in a modal therefore resolved
   `background: var(--ts-brand)` to nothing and fell back to transparent, while
   the `color: #fff` on the primary variant is a literal and survived: a white
   label on a white modal, i.e. an invisible Create button. The only part of it
   that rendered was the box-shadow, which is also a literal.

   Moving the modals inside <main> would fix it, but a fixed-position dialog
   inside a page wrapper is a clipping bug waiting for the first ancestor that
   grows a transform. Adding .tstudio to the modal instead would ALSO switch on
   every `.tstudio X` rule in this sheet - which squares Bootstrap buttons, and
   the Sandbox modals hold a dozen of those. So: a second selector on the token
   blocks and nothing else. It declares the palette and changes no component. */
.tstudio,
.ts-tokens {
  --ts-ink:    #1e293b;
  --ts-muted:  #64748b;
  --ts-faint:  #94a3b8;
  --ts-line:   #e2e8f0;
  --ts-card:   #fff;
  --ts-soft:   #f8fafc;
  --ts-head:   rgba(241, 245, 249, .7);
  --ts-brand:  #4f46e5;   /* fills: white sits on this at 6.3:1 */
  --ts-brand-t:#4f46e5;   /* the same colour as TEXT in light mode */
  --ts-brand-d:#4338ca;
  --ts-brand-s:#eef2ff;
}
[data-bs-theme="dark"] .tstudio,
[data-bs-theme="dark"] .ts-tokens {
  --ts-ink:    #e8ecf7;
  --ts-muted:  #98a2be;
  --ts-faint:  #7c869f;
  --ts-line:   #2b3247;
  --ts-card:   #171c2c;
  --ts-soft:   #131826;
  --ts-head:   rgba(28, 34, 51, .7);
  /* The fill keeps the light indigo so white-on-it stays 6.3:1; only the TEXT variant
     lifts. #6366f1 as text on the #171c2c card measured 3.79:1. */
  --ts-brand:  #4f46e5;
  --ts-brand-t:#a5b4fc;
  --ts-brand-d:#4f46e5;
  --ts-brand-s:rgba(129, 140, 248, .14);
}

/* ---------- hero ---------- */
.ts-hero { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
.ts-hero-txt { flex: 1 1 20rem; min-width: 0; }
.ts-hero h1 { margin: 0; display: flex; align-items: center; gap: .7rem;
  font-size: clamp(1.25rem, 2.4vw, 1.6rem); font-weight: 800; letter-spacing: -.02em;
  color: var(--ts-ink); }
.ts-hero h1 em { font-style: normal; color: var(--ts-brand-t); }
.ts-hero p { margin: .3rem 0 0; font-size: .86rem; color: var(--ts-muted); }
/* font-size is NOT optional here. Font Awesome's SVG build replaces the icon span with an
   <svg height: 1em>, so the glyph is sized by whatever text size it inherits. Beside a heading
   that is ~16px and fits; inside one (the interview pages put the mark in a 46px h1) it renders
   52x46 inside this 36px tile, spilling a white glyph onto a white page - the icon simply
   vanishes. Pinning the size makes the tile self-contained wherever it is used. */
.ts-mark {
  flex: 0 0 auto; width: 2.25rem; height: 2.25rem; border-radius: .75rem;
  display: grid; place-items: center; color: #fff; background: var(--ts-brand);
  font-size: 1rem; line-height: 1;
  box-shadow: 0 4px 12px rgba(79, 70, 229, .25);
}
/* Belt and braces: whatever FA emits, it stays inside the tile. */
.ts-mark > svg, .ts-mark > span { max-width: 100%; max-height: 100%; }

/* The hero mark in the tool's own colour, applied by js/jch-tools.js from the catalogue.
   Solid, not the pale tint the cards use: this is the heaviest mark on the page and a 13%
   wash would make it the lightest thing in the heading. It takes the tone's mid-colour as a
   fill and keeps the white glyph, so the hue matches the card while the weight does not.

   Falls back to the brand indigo, which is what pages outside the catalogue keep. */
.ts-mark.toned {
  background: var(--jch-ti-fg, var(--ts-brand));
  box-shadow: 0 4px 12px color-mix(in srgb, var(--jch-ti-fg, var(--ts-brand)) 30%, transparent);
}
.ts-ver { font-size: .62rem; font-weight: 800; text-transform: uppercase; letter-spacing: .04em;
  padding: .1rem .45rem; border-radius: 999px;
  color: var(--ts-brand-d); background: var(--ts-brand-s);
  border: 1px solid color-mix(in srgb, var(--ts-brand) 25%, transparent); }
[data-bs-theme="dark"] .ts-ver { color: #c7d2fe; }

.ts-hero-side { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
/* The claim is literally true - no request leaves the page - so it is worth stating. */
.ts-private { display: inline-flex; align-items: center; gap: .45rem;
  font-size: .74rem; font-weight: 600; color: var(--ts-muted);
  background: var(--ts-soft); border: 1px solid var(--ts-line);
  padding: .35rem .75rem; border-radius: 999px; }
.ts-private i { width: .5rem; height: .5rem; border-radius: 50%; background: #10b981; }

/* ---------- toolbar ---------- */
.ts-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  background: var(--ts-card); border: 1px solid var(--ts-line);
  border-radius: 1rem; padding: .75rem; margin-bottom: .9rem;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
}
/* Square, matching the sign-in and sign-up submit buttons.

   Applied to the whole .ts-btn family rather than only the primary one: on these pages the
   primary sits directly beside its secondaries in the same toolbar row, so squaring one alone
   would read as a mistake rather than a decision. The controls around them - selects, checkboxes,
   the segmented toggles, the pane-header minis and the icon buttons - keep their radii, so the
   buttons stay the square things you press. */
.ts-btn {
  display: inline-flex; align-items: center; gap: .5rem; cursor: pointer;
  border-radius: 0; font-size: .84rem; font-weight: 600; padding: .55rem .9rem;
  border: 1px solid var(--ts-line); background: var(--ts-soft); color: var(--ts-ink);
  transition: background .14s ease, border-color .14s ease, color .14s ease;
}
.ts-btn:hover { background: color-mix(in srgb, var(--ts-ink) 6%, var(--ts-card)); }
/* TWO spellings of the same button, because the markup uses both and only one of them was ever
   defined. Four pages - the REST/SOAP listener, the Database Lab, the API tester and the Spring
   Boot + Database sandbox - write class="ts-btn ts-btn-primary", which matched nothing at all:
   every call to action on those pages, New endpoint and Create and Save changes among them, has
   been rendering as an ordinary secondary button.

   Aliased here rather than corrected in the four pages' markup: one rule fixes every occurrence
   including the ones generated by script, and it cannot drift out of step the way four copies can.
   .ts-btn.ts-btn-primary rather than a bare .ts-btn-primary, so the alias carries the same
   specificity as the original - a bare single class would lose to .ts-btn:hover and the button
   would go grey under the pointer. */
.ts-btn.primary,
.ts-btn.ts-btn-primary {
  border-color: transparent; color: #fff; background: var(--ts-brand); font-weight: 700;
  box-shadow: 0 4px 12px rgba(79, 70, 229, .22);
}
.ts-btn.primary:hover,
.ts-btn.ts-btn-primary:hover { background: var(--ts-brand-d); }
.ts-btn.primary:active,
.ts-btn.ts-btn-primary:active { transform: scale(.985); }
/* The shortcut is printed on the button, so it is discoverable without a tooltip. */
.ts-kbd { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .64rem; font-weight: 600; padding: .12rem .35rem; border-radius: .3rem;
  background: rgba(0, 0, 0, .22); color: #e0e7ff; }
@media (max-width: 991.98px) { .ts-kbd { display: none; } }

.ts-opts { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-left: auto; }
.ts-sel {
  appearance: none; -webkit-appearance: none; cursor: pointer;
  font-size: .78rem; font-weight: 600; padding: .5rem 1.9rem .5rem .7rem;
  border-radius: .7rem; border: 1px solid var(--ts-line);
  background: var(--ts-soft); color: var(--ts-ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2394a3b8'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right .5rem center; background-size: 1rem;
}
.ts-sel:focus { outline: none; border-color: var(--ts-brand);
  box-shadow: 0 0 0 .18rem color-mix(in srgb, var(--ts-brand) 22%, transparent); }
.ts-check {
  display: inline-flex; align-items: center; gap: .45rem; cursor: pointer; margin: 0;
  font-size: .78rem; font-weight: 600; color: var(--ts-muted);
  padding: .45rem .7rem; border-radius: .7rem;
  border: 1px solid var(--ts-line); background: var(--ts-soft);
}
.ts-check input { width: .95rem; height: .95rem; accent-color: var(--ts-brand); cursor: pointer; }
/* Destructive, so it is an icon on its own rather than a fourth button in the row - and it only
   turns red on approach, which is the moment the warning is useful. */
.ts-icon {
  display: inline-grid; place-items: center; width: 2.2rem; height: 2.2rem; cursor: pointer;
  font-size: .9rem;
  border-radius: .7rem; border: 1px solid transparent; background: none; color: var(--ts-faint);
  transition: background .14s ease, color .14s ease;
}
.ts-icon:hover { color: #e11d48; background: rgba(225, 29, 72, .1); }

/* ---------- panes ---------- */
.tstudio .jx-split { gap: .9rem; background: none; border: 0; }
.tstudio .jx-pane {
  background: var(--ts-card); border: 1px solid var(--ts-line); border-radius: 1rem;
  overflow: hidden; box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
  transition: border-color .14s ease, box-shadow .14s ease;
}
.tstudio .jx-pane.left:focus-within { border-color: var(--ts-brand);
  box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--ts-brand) 16%, transparent); }
.tstudio .jx-head {
  display: flex; align-items: center; gap: .6rem; padding: .6rem .9rem;
  background: var(--ts-head); border-bottom: 1px solid var(--ts-line);
  font-size: .7rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--ts-muted);
}
/* Amber for what you give it, green for what it gives back - the two panes are otherwise
   identical rectangles and the labels alone read as a wall of small caps. */
.tstudio .jx-head .dot { width: .5rem; height: .5rem; border-radius: 50%; flex: 0 0 auto; }
.tstudio .jx-head .dot.in  { background: #f59e0b; }
.tstudio .jx-head .dot.out { background: #10b981; }
.tstudio .jx-head .ttl { letter-spacing: .05em; }
.ts-headbtns { margin-left: auto; display: flex; align-items: center; gap: .45rem;
  text-transform: none; letter-spacing: 0; }
.ts-mini {
  display: inline-flex; align-items: center; gap: .35rem; cursor: pointer;
  font-size: .7rem; font-weight: 600; padding: .28rem .6rem; border-radius: .45rem;
  border: 1px solid var(--ts-line); background: var(--ts-card); color: var(--ts-muted);
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.ts-mini:hover { color: var(--ts-brand-t); border-color: color-mix(in srgb, var(--ts-brand) 40%, transparent); }
.ts-mini.accent { color: var(--ts-brand-d); background: var(--ts-brand-s);
  border-color: color-mix(in srgb, var(--ts-brand) 28%, transparent); font-weight: 700; }
[data-bs-theme="dark"] .ts-mini.accent { color: #c7d2fe; }
.ts-mini.ok { color: #059669; border-color: rgba(16, 185, 129, .45); background: rgba(16, 185, 129, .1); }
[data-bs-theme="dark"] .ts-mini.ok { color: #4ade80; }

.tstudio .jx-body { background: var(--ts-card); }
.tstudio .jx-pane.right .jx-body { background: var(--ts-soft); }
.tstudio .jx-in, .tstudio .jx-out {
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .85rem; line-height: 1.65; color: var(--ts-ink); background: transparent;
  padding: 1rem;
}
.tstudio .jx-in::placeholder { color: var(--ts-faint); }

/* ---------- status bar ---------- */
.ts-stats {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.4rem;
  background: var(--ts-card); border: 1px solid var(--ts-line); border-radius: .75rem;
  padding: .6rem 1rem; margin-top: .9rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .74rem; color: var(--ts-muted);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
}
.ts-stats b { font-weight: 700; color: var(--ts-ink); font-variant-numeric: tabular-nums; }
.ts-stats .dialect b { color: var(--ts-brand-t); }
.ts-stats .verdict { margin-left: auto; font-weight: 600; }
/* #059669 measures 3.77:1 on the white stat card - the one status a reader most wants to
   glance at, so it gets the darker green. This is 5.5:1. */
.ts-stats .verdict.ok   { color: #047857; }
.ts-stats .verdict.bad  { color: #e11d48; }
[data-bs-theme="dark"] .ts-stats .verdict.ok  { color: #4ade80; }
[data-bs-theme="dark"] .ts-stats .verdict.bad { color: #fda4af; }

@media (max-width: 767.98px) {
  .ts-opts { margin-left: 0; width: 100%; }
  .ts-sel { flex: 1 1 8rem; }
}
@media (prefers-reduced-motion: reduce) {
  .ts-btn, .ts-mini, .ts-icon, .tstudio .jx-pane { transition: none; }
  .ts-btn.primary:active, .ts-btn.ts-btn-primary:active { transform: none; }
}

/* ---------- segmented view toggle ----------
   Two radios styled as one control. Radios rather than buttons because the two views are
   mutually exclusive and a screen reader should hear a group with a chosen member, not two
   unrelated toggles. */
.ts-seg { display: inline-flex; padding: .2rem; gap: .15rem; border-radius: .7rem;
  border: 1px solid var(--ts-line); background: var(--ts-soft); }
.ts-seg input { position: absolute; opacity: 0; pointer-events: none; }
.ts-seg label {
  display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; margin: 0;
  font-size: .76rem; font-weight: 600; padding: .35rem .7rem; border-radius: .5rem;
  color: var(--ts-muted); transition: background .14s ease, color .14s ease;
}
.ts-seg input:checked + label { background: var(--ts-card); color: var(--ts-brand-t);
  box-shadow: 0 1px 3px rgba(15, 23, 42, .12); }
.ts-seg input:focus-visible + label { outline: 2px solid var(--ts-brand); outline-offset: 1px; }

/* ---------- caveat line under a toolbar ----------
   For a limitation the reader needs BEFORE they trust the output, not after. */
/* Darker than --ts-muted on purpose: this is the only muted line that sits on the PAGE
   ground rather than on a white card, where the generic muted drops to 4.43:1. This is 5.9. */
.ts-note { margin: -.3rem 0 .9rem; font-size: .78rem; line-height: 1.55; color: #55607a; }
[data-bs-theme="dark"] .ts-note { color: var(--ts-muted); }
.ts-note .fa-solid { color: var(--ts-faint); margin-right: .35rem; }

/* ---------- two-way panes ----------
   On a converter either pane can be the source, so a fixed in/out colour would be wrong half
   the time. The dots stay neutral until a conversion runs, then mark which side was read and
   which was written. */
.tstudio .jx-head .dot.idle { background: var(--ts-faint); }
.tstudio .jx-pane.is-src .jx-head .dot { background: #f59e0b; }
.tstudio .jx-pane.is-dst .jx-head .dot { background: #10b981; }
.tstudio .jx-pane .jx-head .role { font-weight: 600; letter-spacing: 0; opacity: .7;
  text-transform: none; }

/* A figure carried in a pane header rather than the status bar, for a number that belongs
   to that pane alone. */
.ts-headstat { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .68rem; font-weight: 600; color: var(--ts-faint); text-transform: none;
  letter-spacing: 0; }

/* ---------- the /pattern/flags bar ----------
   The regex tester's primary input is a field, not a button, so it takes the whole first row
   of the toolbar. The delimiters are drawn rather than typed: they are not part of the
   pattern, and a reader who types them gets a regex that matches literal slashes. */
.ts-rx {
  display: flex; align-items: stretch; flex: 1 1 22rem; min-width: 0;
  border: 1px solid var(--ts-line); border-radius: .7rem; overflow: hidden;
  background: var(--ts-card);
  transition: border-color .14s ease, box-shadow .14s ease;
}
.ts-rx:focus-within { border-color: var(--ts-brand);
  box-shadow: 0 0 0 .18rem color-mix(in srgb, var(--ts-brand) 20%, transparent); }
.ts-rx.invalid { border-color: #dc2626;
  box-shadow: 0 0 0 .18rem rgba(220, 38, 38, .16); }
[data-bs-theme="dark"] .ts-rx.invalid { border-color: #fda4af;
  box-shadow: 0 0 0 .18rem rgba(253, 164, 175, .18); }
.ts-rx .slash, .ts-rx .ts-rx-flags {
  display: flex; align-items: center; padding: 0 .6rem;
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 700; background: var(--ts-soft); color: var(--ts-faint);
}
.ts-rx .ts-rx-flags { font-size: .82rem; color: var(--ts-brand-t); min-width: 3rem; }
.ts-rx input {
  flex: 1 1 auto; min-width: 0; border: 0; outline: none; background: transparent;
  padding: .55rem .5rem; color: var(--ts-ink);
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .875rem;
}
.ts-rx input::placeholder { color: var(--ts-faint); }

/* ---------- flag chips ----------
   Six independent toggles, so checkboxes rather than the radio group .ts-seg uses. Each keeps
   its letter, because the letter is what ends up in the pattern. */
.ts-flags { display: inline-flex; gap: .2rem; padding: .2rem; border-radius: .7rem;
  border: 1px solid var(--ts-line); background: var(--ts-soft); }
.ts-flags input { position: absolute; opacity: 0; pointer-events: none; }
.ts-flags label {
  display: grid; place-items: center; cursor: pointer; margin: 0;
  width: 1.7rem; height: 1.7rem; border-radius: .45rem;
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .78rem; font-weight: 700; color: var(--ts-muted);
  transition: background .14s ease, color .14s ease;
}
.ts-flags label:hover { color: var(--ts-ink); }
.ts-flags input:checked + label { background: var(--ts-brand); color: #fff; }
.ts-flags input:focus-visible + label { outline: 2px solid var(--ts-brand); outline-offset: 1px; }

/* A field that only matters while its toggle is on. */
.ts-inline-field {
  flex: 1 1 12rem; min-width: 0; padding: .5rem .7rem; border-radius: .7rem;
  border: 1px solid var(--ts-line); background: var(--ts-card); color: var(--ts-ink);
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .8rem;
}
.ts-inline-field:focus { outline: none; border-color: var(--ts-brand);
  box-shadow: 0 0 0 .18rem color-mix(in srgb, var(--ts-brand) 20%, transparent); }
.ts-inline-field:disabled { opacity: .5; cursor: not-allowed; }
.ts-inline-field::placeholder { color: var(--ts-faint); }

/* ---------- a highlighted single result ----------
   For a tool that produces many of something and wants to show ONE of them properly - the
   UUID generator dissecting its first value into timestamp / version / random. */
.ts-feature {
  background: var(--ts-soft); border: 1px solid var(--ts-line); border-radius: 1rem;
  padding: 1rem 1.15rem; margin-bottom: .9rem;
}
.ts-feature h2 {
  margin: 0 0 .5rem; font-size: .68rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ts-muted);
}

/* A small number field inside a .ts-check, for "how many" style options. Its label sits beside
   it rather than above, so the whole control stays one chip in the toolbar row. */
.ts-num {
  width: 4.2rem; padding: .15rem .35rem; border-radius: .35rem;
  border: 1px solid var(--ts-line); background: var(--ts-card); color: var(--ts-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .76rem; font-weight: 600;
}
.ts-num:focus { outline: none; border-color: var(--ts-brand);
  box-shadow: 0 0 0 .12rem color-mix(in srgb, var(--ts-brand) 22%, transparent); }

/* ---------- a verdict that is still a badge ----------
   Eight pages write plain text into .verdict; the eleven skinned in bulk still render Bootstrap
   badges into it from their own JS. Rewriting eleven result-renderers to match would risk far
   more than it gains, so the chip is made to sit properly in the stat row instead. */
.ts-stats .verdict .badge {
  font-family: inherit; font-size: .72rem; font-weight: 600;
  padding: .18rem .5rem; border-radius: .35rem; vertical-align: baseline;
}
.ts-stats .verdict:empty::before { content: "Ready"; color: var(--ts-muted); }

/* ---------- "More free tools" ----------
   A full-width grid under the article rather than a column beside it. auto-fill with a
   min track means the row count follows the viewport instead of a breakpoint guess. */
.ts-more { margin: 1.5rem 0 2rem; }
.ts-more > h2 {
  margin: 0 0 .9rem; font-size: .72rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ts-muted);
}
.ts-more-grid {
  display: grid; gap: .7rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15.5rem), 1fr));
}
.ts-more-card {
  display: flex; align-items: flex-start; gap: .7rem; text-decoration: none;
  padding: .8rem .9rem; border-radius: .75rem;
  background: var(--ts-card); border: 1px solid var(--ts-line);
  transition: border-color .14s ease, transform .14s ease, box-shadow .14s ease;
}
.ts-more-card:hover { border-color: color-mix(in srgb, var(--ts-brand) 45%, transparent);
  transform: translateY(-2px); box-shadow: 0 6px 14px -6px rgba(15, 23, 42, .18); }
/* Neutral tile on purpose: every entry in JCH.tools carries its own colour class
   (text-primary-emphasis, text-success-emphasis, ...), and an indigo-tinted tile behind
   nineteen different hues fights all of them. The tile gives the grid a shared shape; the
   icon keeps its identity. */
/* Tinted per tool, from the same .jch-ti-<tone> classes the navbar menu and the All Tools
   page use. It was one neutral tile for every tool, so the strip listed nineteen tools that
   all looked alike and none of them looked like their own entry in the menu.

   Fallback kept: the tone classes ship in the stylesheet the navbar injects. */
.ts-more-ico {
  flex: 0 0 auto; width: 2rem; height: 2rem; border-radius: .5rem; display: grid;
  place-items: center; font-size: .85rem;
  background: var(--jch-ti-bg, color-mix(in srgb, var(--ts-ink) 6%, var(--ts-card)));
  color: var(--jch-ti-fg, var(--ts-ink));
}
.ts-more-txt { min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.ts-more-txt b { font-size: .84rem; font-weight: 700; color: var(--ts-ink); }
.ts-more-txt span { font-size: .74rem; line-height: 1.45; color: var(--ts-muted);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ts-more-card .badge { align-self: flex-start; }
@media (prefers-reduced-motion: reduce) {
  .ts-more-card { transition: none; }
  .ts-more-card:hover { transform: none; }
}

/* ---------- IDE pane titles ----------
   The compiler and sandbox label their panes with .pane-title rather than the .jx-head the
   tools use, so the dot gets its own hook there. Same vocabulary: amber is what you write,
   green is what came back. */
.tstudio .pane-title .dot {
  display: inline-block; width: .5rem; height: .5rem; border-radius: 50%;
  margin-right: .45rem; vertical-align: baseline;
}
.tstudio .pane-title .dot.in  { background: #f59e0b; }
.tstudio .pane-title .dot.out { background: #10b981; }

/* The IDE toolbars are a flex row of buttons like a .ts-bar, but they must not take the
   card chrome - they sit directly above an editor that fills the viewport. */
.tstudio .ide-toolbar .ts-btn { padding: .4rem .8rem; font-size: .82rem; }
.tstudio .ide-toolbar .ts-btn.primary,
.tstudio .ide-toolbar .ts-btn.ts-btn-primary { font-weight: 700; }

/* Execute, and the token top-up. Both keep the hue they carried before the skin: green
   already means "run this" across the site, and amber is what the nav uses for tokens. */
/* #16a34a was the obvious green and it is wrong here: white on it is 3.3:1, WORSE than the
   Bootstrap btn-success (#198754, 4.53) this replaced. A filled button carrying 13px text needs
   4.5, so the fill goes darker. This is 5.0:1, and the hover 7.1. */
.ts-btn.run {
  border-color: transparent; color: #fff; background: #15803d; font-weight: 700;
  box-shadow: 0 4px 12px rgba(21, 128, 61, .25);
}
.ts-btn.run:hover { background: #166534; }
.ts-btn.warn {
  border-color: transparent; color: #3d2801; background: #fbbf24; font-weight: 700;
}
.ts-btn.warn:hover { background: #f59e0b; }

/* ---------- quiz chrome ----------
   The interview pages open on a setup screen that is centred by design - it is a choice to
   make, not a workspace to get out of the way - so the hero keeps that alignment rather than
   being forced left like the formatter tools. */
.ts-hero.is-centred { flex-direction: column; align-items: center; text-align: center;
  gap: .6rem; }
.ts-hero.is-centred .ts-hero-txt { flex: 0 1 auto; }
.ts-hero.is-centred .ts-hero-side { justify-content: center; }

/* A chosen option among several - the quiz length buttons. Distinct from .ts-btn.primary,
   which is an action rather than a selection. */
.ts-btn.active {
  border-color: var(--ts-brand); color: var(--ts-brand-t);
  background: var(--ts-brand-s); font-weight: 700;
}
.ts-btn.lg { padding: .7rem 1.4rem; font-size: .95rem; }

/* Ending a session is destructive enough to keep its red, but it carries a full label
   ("End & get my report") so it stays a button rather than becoming a .ts-icon. */
.ts-btn.danger {
  border-color: rgba(225, 29, 72, .35); background: rgba(225, 29, 72, .08); color: #be123c;
}
.ts-btn.danger:hover { background: #e11d48; border-color: transparent; color: #fff; }
[data-bs-theme="dark"] .ts-btn.danger { color: #fda4af; }

/* The 50:50 lifeline. Amber because it is a limited resource, and because the attract pulse
   that fires on the first question is amber too - a neutral button under an amber pulse reads
   as two unrelated things. Tinted rather than filled: it sits in a busy question header and a
   solid fill there would outrank the question. */
.ts-btn.amber {
  border-color: rgba(245, 158, 11, .4); background: rgba(245, 158, 11, .12); color: #92400e;
  font-weight: 700;
}
.ts-btn.amber:hover { background: rgba(245, 158, 11, .22); color: #78350f; }
[data-bs-theme="dark"] .ts-btn.amber { color: #fcd34d; border-color: rgba(251, 191, 36, .4); }

/* ============================================================================
   Interview setup screens
   ----------------------------------------------------------------------------
   Lifted out of ai-interview.html so the four quiz pages share one definition rather than a
   second copy that drifts. The .ai- prefix is historical: it was written for the AI
   interviewer and is now worn by every interview setup screen.
   ========================================================================= */
/* ===== setup screen, rebuilt to the reference ===== */
.ai-steps { max-width: 64rem; margin: 0 auto; }

/* Bottom action banner. Hosts the LIVE cost line rather than a fixed "free" claim -
   what this costs depends on whether you are signed in and how many tokens you hold. */
.ai-launch { display: flex; flex-wrap: wrap; align-items: center; gap: 1.25rem;
  background: var(--phoenix-body-emphasis-bg, #fff);
  border: 1px solid var(--phoenix-border-color, #e3e6ed);
  border-radius: 1rem; padding: 1.25rem 1.4rem; margin-top: 1.75rem;
  box-shadow: 0 1px 3px rgba(15, 23, 42, .05); }
.ai-launch-ico { flex: 0 0 auto; width: 3rem; height: 3rem; border-radius: 1rem;
  display: grid; place-items: center; font-size: 1.15rem;
  background: #eef2ff; border: 1px solid #e0e7ff; color: #4f46e5; }
[data-bs-theme="dark"] .ai-launch-ico { background: rgba(129,140,248,.16);
  border-color: rgba(129,140,248,.3); color: #a5b4fc; }
@media (max-width: 575.98px) { .ai-launch-ico { display: none; } }
.ai-launch-txt { flex: 1 1 16rem; min-width: 0; }
.ai-launch #btn-start { flex: 0 0 auto; }
@media (max-width: 575.98px) { .ai-launch #btn-start { width: 100%; justify-content: center; } }

/* Hero */
.ai-badge { display: inline-flex; align-items: center; gap: .5rem;
  padding: .35rem .8rem; border-radius: 999px; font-size: .74rem; font-weight: 600;
  background: #eef2ff; border: 1px solid #e0e7ff; color: #4338ca; }
[data-bs-theme="dark"] .ai-badge { background: rgba(129,140,248,.14);
  border-color: rgba(129,140,248,.3); color: #c7d2fe; }
.ai-badge .dot { position: relative; width: .5rem; height: .5rem; }
.ai-badge .dot i { position: absolute; inset: 0; border-radius: 50%; background: #4f46e5; }
.ai-badge .dot i:first-child { animation: aiPing 1.9s cubic-bezier(0,0,.2,1) infinite;
  opacity: .75; }
@keyframes aiPing { 75%, 100% { transform: scale(2.2); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .ai-badge .dot i:first-child { animation: none; } }
/* Sized like the reference, which is a good deal larger than the fs-4 this page used -
   the headline is the first thing on a page whose whole job is to be started. */
.ai-h1 { font-size: clamp(1.75rem, 4.2vw, 2.9rem); line-height: 1.12;
  letter-spacing: -.03em; }
.ai-grad { background: linear-gradient(90deg, #4f46e5, #7c3aed);
  -webkit-background-clip: text; background-clip: text; color: transparent; }
.ai-benefits { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem 1.5rem;
  font-size: .76rem; font-weight: 500;
  color: var(--phoenix-body-tertiary-color, #64748b); }
.ai-benefits span { display: inline-flex; align-items: center; gap: .4rem; }
.ai-benefits .fa-circle-check { color: #10b981; }
.ai-step-head { display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin: 0 0 .85rem; }
.ai-step-head .lhs { display: flex; align-items: center; gap: .65rem; min-width: 0; }
.ai-step-n { flex: 0 0 auto; width: 1.5rem; height: 1.5rem; border-radius: 50%;
  display: grid; place-items: center; background: #4f46e5; color: #fff;
  font-size: .72rem; font-weight: 800; }
.ai-step-head h2 { margin: 0; font-size: 1rem; font-weight: 700; letter-spacing: -.01em;
  color: var(--phoenix-emphasis-color, #0f172a); }
/* The hint says what the choice DOES, which is the question a reader actually has here. */
.ai-step-hint { flex: 0 0 auto; font-size: .74rem; font-weight: 500;
  color: var(--phoenix-body-tertiary-color, #94a3b8); }
@media (max-width: 575.98px) { .ai-step-hint { display: none; } }

/* ===== selection card ===== */
.ai-pick { position: relative; cursor: pointer; height: 100%; overflow: hidden;
  border: 1px solid var(--phoenix-border-color, #e3e6ed); border-radius: 1rem;
  padding: 1rem; background: var(--phoenix-body-emphasis-bg, #fff);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
  display: flex;
  transition: border-color .18s, box-shadow .18s, transform .18s, background-color .18s; }

/* No gradient wash. The reference marks the chosen card with a 2px indigo border and a
   lift; a tint on top of that is a third signal saying the same thing, and it fought the
   per-track icon colours underneath. */
.ai-pick:hover { transform: translateY(-2px); border-color: #cbd5e1;
  box-shadow: 0 .5rem 1.1rem rgba(15, 23, 42, .08); }
.ai-pick:focus-visible { outline: 2px solid var(--phoenix-primary, #3874ff); outline-offset: 2px; }

.ai-pick.sel { border-color: #4f46e5; box-shadow: 0 0 0 1px #4f46e5,
  0 .5rem 1.2rem rgba(79, 70, 229, .18); transform: translateY(-2px); }

/* Icon chip. Greyscale until chosen, then it takes the gradient - the one element that
   changes colour on selection, so which card is picked is readable at a glance. */
/* One tone per option, kept whether or not it is selected. The reference gives each card
   its own colour so the grid reads as a set of distinct choices rather than a repeated
   control; selection is carried by the border and the check instead. */
.ai-pick .ico { position: relative; z-index: 1; width: 2.5rem; height: 2.5rem;
  flex: 0 0 2.5rem; border-radius: .7rem; display: flex; align-items: center;
  justify-content: center; font-size: 1rem;
  background: #eef2ff; border: 1px solid #e0e7ff; color: #4f46e5;
  transition: transform .2s; }
.ai-pick[data-tone="amber"]  .ico { background: #fffbeb; border-color: #fde68a; color: #d97706; }
.ai-pick[data-tone="emerald"] .ico { background: #ecfdf5; border-color: #a7f3d0; color: #059669; }
.ai-pick[data-tone="rose"]   .ico { background: #fff1f2; border-color: #fecdd3; color: #e11d48; }
.ai-pick[data-tone="teal"]   .ico { background: #f0fdfa; border-color: #99f6e4; color: #0d9488; }
.ai-pick[data-tone="purple"] .ico { background: #faf5ff; border-color: #e9d5ff; color: #9333ea; }
[data-bs-theme="dark"] .ai-pick .ico { background: rgba(129,140,248,.16);
  border-color: rgba(129,140,248,.3); color: #a5b4fc; }
[data-bs-theme="dark"] .ai-pick[data-tone="amber"]  .ico { background: rgba(251,191,36,.16); border-color: rgba(251,191,36,.3); color: #fbbf24; }
[data-bs-theme="dark"] .ai-pick[data-tone="emerald"] .ico { background: rgba(16,185,129,.16); border-color: rgba(16,185,129,.3); color: #34d399; }
[data-bs-theme="dark"] .ai-pick[data-tone="rose"]   .ico { background: rgba(244,63,94,.16); border-color: rgba(244,63,94,.3); color: #fda4af; }
[data-bs-theme="dark"] .ai-pick[data-tone="teal"]   .ico { background: rgba(20,184,166,.16); border-color: rgba(20,184,166,.3); color: #5eead4; }
[data-bs-theme="dark"] .ai-pick[data-tone="purple"] .ico { background: rgba(168,85,247,.16); border-color: rgba(168,85,247,.3); color: #d8b4fe; }
/* The tile keeps its own tone when chosen. It used to flip to a blue-violet gradient,
   which meant the selected card lost the colour that identifies it - Kafka stopped
   being rose the moment you picked Kafka. Selection is the border and the check. */
.ai-pick.sel .ico { transform: scale(1.04); }

.ai-pick .body { position: relative; z-index: 1; min-width: 0; }
.ai-pick .t { font-weight: 700; font-size: .88rem; line-height: 1.25; }
.ai-pick.stack .s { margin-top: .15rem; }
.ai-pick .s { font-size: .74rem; color: var(--phoenix-body-tertiary-color, #6b7280); }

/* STACK - a track. Icon and check share the top line, the words sit beneath, and the card
   stretches so four of them in a row end level whatever the blurb length. */
.ai-pick.stack { flex-direction: column; justify-content: space-between; gap: 1rem; }
.ai-pick.stack .top { display: flex; align-items: flex-start;
  justify-content: space-between; }
.ai-pick.stack .tick { position: static; }

/* LINE - a level or a length. One line: icon and words on the left, check on the right.
   NOT called .row: that is a Bootstrap class, and .ai-pick.row inherited its flex-wrap and
   negative margins, which pushed the check onto a second line inside the card. */
.ai-pick.line { flex-direction: row; align-items: center;
  justify-content: space-between; gap: .75rem; }
.ai-pick.line .lhs { display: flex; align-items: center; gap: .75rem; min-width: 0; }
.ai-pick.line .tick { position: static; flex: 0 0 auto; }

/* Tick on the selected card. Scales in rather than appearing, so the selection has a beat. */
.ai-pick .tick { position: absolute; top: .5rem; right: .55rem; z-index: 1;
  width: 17px; height: 17px; border-radius: 50%; display: flex; align-items: center;
  justify-content: center; font-size: .55rem; color: #fff;
  background: #4f46e5;
  transform: scale(0); transition: transform .2s cubic-bezier(.2, 1.5, .4, 1); }
.ai-pick.sel .tick { transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  .ai-mark-face { animation: none; }
  .ai-pick, .ai-pick .ico, .ai-pick .tick { transition: none; }
  .ai-pick:hover, .ai-pick.sel { transform: none; }
}

/* ---------- Bootstrap pagination, squared ----------
   The notifications pager is Bootstrap's .pagination component, not the hand-rolled .tp-page /
   .pj-page strips, so it needs an override rather than a changed declaration. Bootstrap rounds
   the outer cells with :first-child / :last-child longhands at a high specificity, which a plain
   border-radius shorthand loses to - hence the longhands here too.

   Scoped to .tstudio so it only reaches pages wearing this skin. */
.tstudio .pagination .page-item .page-link,
.tstudio .pagination .page-item:first-child .page-link,
.tstudio .pagination .page-item:last-child .page-link {
  border-radius: 0;
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}

/* ============================================================================
   Bootstrap buttons inside a studio page
   ----------------------------------------------------------------------------
   Not every control on these pages was swept into .ts-btn - the compiler toolbars and the
   profile account cards still use Bootstrap's own buttons - so a page could show a square
   Run beside a round Reset. Scoped to .tstudio, so nothing outside these pages moves.

   Excluded: .btn-close (a glyph in a circle, Bootstrap's own affordance), anything asking
   for .rounded-pill, and .dropdown-item, which is a menu row rather than a button.

   Longhands rather than the shorthand: Bootstrap gives .btn-group children per-corner radii
   through :first-child/:last-child, and a shorthand loses to those at equal specificity.
   ========================================================================= */
.tstudio .btn:not(.btn-close):not(.rounded-pill):not(.dropdown-item),
.tstudio .btn-group > .btn:not(.btn-close):not(.rounded-pill) {
  border-top-left-radius: 0; border-top-right-radius: 0;
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}

/* ============================================================================
   The Developer Lab usage meter
   ----------------------------------------------------------------------------
   Here rather than in css/developer-lab.css because two pages render it - the
   listener console and the Database Lab - and both already load this sheet.

   It used to be a row of Bootstrap utilities: a thin bordered strip with the
   whole sentence in one 13px grey run. Every figure in it was equally quiet,
   including the one the reader came for. Now the first clause is the headline,
   the rest is small print under it, and the same number is drawn as a bar, a
   percentage and a ring on the right - so "how much is left" can be read
   without reading at all.

   The percentage arrives as --pct on the wrapper, set by jch-devlab-meter.js.
   ========================================================================== */
.jch-meter {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .9rem 1.1rem; margin-bottom: 1.1rem;
  background: var(--ts-card); border: 1px solid var(--ts-line); border-radius: 1.1rem;
}
.jch-meter[data-tone="warn"] { border-color: rgba(245, 158, 11, .5); }

.jch-meter-ico {
  flex: 0 0 auto; width: 2.7rem; height: 2.7rem; border-radius: 50%;
  display: grid; place-items: center; font-size: 1rem;
  background: var(--ts-brand-s); color: var(--ts-brand-t);
}
.jch-meter[data-tone="warn"] .jch-meter-ico {
  background: rgba(245, 158, 11, .16); color: #b45309;
}
[data-bs-theme="dark"] .jch-meter[data-tone="warn"] .jch-meter-ico { color: #fcd34d; }

.jch-meter-txt { flex: 1 1 15rem; min-width: 0; }
.jch-meter-head { display: block; font-size: .88rem; line-height: 1.45; color: var(--ts-ink); }
.jch-meter-head strong { font-weight: 750; }
.jch-meter-sub { display: block; font-size: .76rem; line-height: 1.5; color: var(--ts-muted); }
.jch-meter-sub strong { font-weight: 700; color: var(--ts-ink); }

/* The gauge: bar, percentage, ring. Three readings of one number - which is why
   the group is aria-hidden and the sentence beside it carries the meaning. */
.jch-meter-gauge { display: flex; align-items: center; gap: .8rem; flex: 1 1 18rem; min-width: 0; }

.jch-meter-bar {
  /* position: relative because the fill is absolutely positioned inside it - see below. */
  position: relative;
  flex: 1 1 auto; min-width: 4rem; height: .5rem; border-radius: .5rem;
  background: color-mix(in srgb, var(--ts-ink) 9%, var(--ts-card)); overflow: hidden;
}
/* ABSOLUTE, not a block in flow, and that is not a styling preference.
   The track is a flex item that gets its width from flex-grow, so its main size is indefinite
   during intrinsic sizing - and a percentage width on an in-flow CHILD of it resolves against
   nothing and collapses to zero. Measured: with --pct at 37 the fill came out 8px, the min-width,
   and a literal 'width: 37%' did exactly the same, which is what ruled the calc() out as the
   culprit. An absolutely positioned box resolves its percentage against the containing block's
   padding box, which is definite once the row has been laid out. */
.jch-meter-bar i {
  position: absolute; top: 0; bottom: 0; left: 0;
  border-radius: .5rem;
  width: calc(var(--pct, 0) * 1%);
  background: var(--ts-brand);
  transition: width .5s cubic-bezier(.22, .61, .36, 1);
}
.jch-meter[data-tone="warn"] .jch-meter-bar i { background: #d97706; }
/* A spent allowance with a zero-width bar looks like a bar that failed to load.
   A sliver says "this is empty", which is the actual state. */
.jch-meter-bar i { min-width: .5rem; }

.jch-meter-pct {
  flex: 0 0 auto; font-size: .76rem; font-weight: 700; color: var(--ts-muted);
  font-variant-numeric: tabular-nums;
}

.jch-meter-ring {
  flex: 0 0 auto; width: 2.9rem; height: 2.9rem; border-radius: 50%;
  display: grid; place-items: center;
  background: conic-gradient(var(--ts-brand) calc(var(--pct, 0) * 1%),
                             color-mix(in srgb, var(--ts-ink) 10%, var(--ts-card)) 0);
}
.jch-meter[data-tone="warn"] .jch-meter-ring {
  background: conic-gradient(#d97706 calc(var(--pct, 0) * 1%),
                             color-mix(in srgb, var(--ts-ink) 10%, var(--ts-card)) 0);
}
/* The hole, and the figure inside it. An inner element rather than a radial
   gradient so the number sits on a solid card colour and stays legible. */
.jch-meter-ring i {
  width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--ts-card);
  display: grid; place-items: center;
  font-style: normal; font-size: .6rem; font-weight: 800; color: var(--ts-ink);
  font-variant-numeric: tabular-nums;
}

.jch-meter-buy {
  flex: 0 0 auto; font-size: .78rem; font-weight: 700; text-decoration: none;
  padding: .4rem .8rem; border-radius: .6rem;
  background: var(--ts-brand); color: #fff;
}
.jch-meter-buy:hover { background: var(--ts-brand-d); color: #fff; }

/* Under 700px the gauge would be a 3rem bar between two round things. The bar
   and the percentage carry it alone; the ring is the first to go. */
@media (max-width: 699.98px) {
  .jch-meter { gap: .75rem; }
  .jch-meter-ring { display: none; }
  .jch-meter-gauge { flex-basis: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .jch-meter-bar i { transition: none; }
}
