/* ============================================================
   Design System Explorer
   ------------------------------------------------------------
   .dsx-*  chrome of the explorer itself (sidebar, stage, panel)
   .ds-*   the actual library components, portable to any page
   ============================================================ */

/* Poppins at the three weights the type scale names: Regular 400,
   Medium 500, SemiBold 600. The explorer used to load Work Sans and
   a Typekit kit alongside it; the kit was referenced by nothing and
   Work Sans was the chrome's whole voice, which was never the
   library's. */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap');

/* ---------- Tokens ----------
   The chrome used to carry a palette of its own: a different blue,
   a near-black the library does not have, a seven-step grey ramp
   and a focus ring drawn as a glow. A site that documents a design
   system and is not built out of it is an argument against the
   system.

   So every name below is an alias onto the library's own token and
   none of them holds a hex. Change the library and the chrome
   follows it. */
:root {
    --ds-blue:      var(--ds-bg-brand);
    --ds-blue-soft: var(--ds-bg-brand-soft);
    --ds-black:     var(--ds-text-primary);
    --ds-white:     var(--ds-bg-background);

    /* Seven greys onto four roles — two grounds and two outlines.
       That the ramp collapses this far is the ramp admitting it was
       never a scale, only a set of values someone needed. */
    --ds-grey-050: var(--ds-bg-grayscale-a);
    --ds-grey-100: var(--ds-bg-grayscale-b);
    --ds-grey-200: var(--ds-outline-tertiary);
    --ds-grey-300: var(--ds-outline-secondary);
    --ds-grey-400: var(--ds-outline-secondary);
    --ds-grey-700: var(--ds-text-primary);

    /* Text roles */
    --ds-text:       var(--ds-text-primary);
    --ds-text-muted: var(--ds-text-secondary);
    /* The library states disabled as a 40% layer, which a colour
       cannot be. This is Text/Secondary at that 40%, flattened onto
       white, for the places the chrome needs a value rather than an
       opacity. */
    --ds-text-disabled: #C8CFD5;

    /* Spacing scale. The library's components carry their own
       measured padding, so this is the chrome's alone. */
    --ds-1: 4px;
    --ds-2: 8px;
    --ds-3: 12px;
    --ds-4: 16px;
    --ds-5: 24px;
    --ds-6: 32px;
    --ds-7: 48px;

    --dsx-sidebar-w: 264px;
    --dsx-header-h: 60px;

    /* The page gutter. The playground band cancels it with a negative
       margin of the same size, so the two can never drift apart. It
       is deliberately narrow: the header's title, the specimen in
       the band and the property panel all sit on this one line. */
    --dsx-main-pad: var(--ds-4);

    /* How wide prose is allowed to get. The band ignores this; every
       other thing in the main column obeys it. */
    --dsx-measure: 1180px;

    /* The playground's own surface. Two values, so a dark mode is a
       pair of overrides rather than a rewrite. */
    --dsx-stage-bg: var(--ds-bg-background);
    --dsx-stage-line: var(--ds-grey-200);

    /* How wide a specimen is allowed to get. A field stretched across
       a 1300px band stops reading as a field: it has to be shown at
       a width it would actually be used at, and 407 is the width the
       input frames draw one at. Three tiers in all — this, a
       component's own playWidth, and playWide for the whole band. */
    --dsx-specimen-w: 407px;
}

/* ---------- Reset-ish ---------- */
*, *::before, *::after { box-sizing: border-box; }

body.dsx {
    margin: 0;
    font-family: var(--ds-font);
    color: var(--ds-text);
    background: var(--ds-grey-050);
    -webkit-font-smoothing: antialiased;
}

/* ============================================================
   Explorer chrome
   ============================================================ */

/* The sidebar runs the full height of the window, so the header
   starts where the sidebar ends. The two rules — the sidebar's
   right edge and the header's underline — meet at one corner, and
   the brand moves into the panel it now sits above. */
.dsx-header {
    position: fixed;
    inset: 0 0 auto var(--dsx-sidebar-w);
    height: var(--dsx-header-h);
    display: flex;
    align-items: center;
    gap: var(--ds-4);
    padding: 0 var(--dsx-main-pad);
    background: color-mix(in srgb, var(--ds-bg-background) 85%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--ds-grey-200);
    z-index: 20;
}

/* Its own row at the top of the panel, exactly the header's height,
   so the brand sits on the header's baseline without a rule under
   it — the sidebar is one surface, not a stack of panels. The row
   also carries the control that collapses the panel to a rail. */
.dsx-brand {
    /* The rail control's glyph is centred in a box wider than
       itself, so the box has to hang past the search field's gutter
       by exactly that inset for the two right edges to land on one
       ruler. --ds-4 here is the gutter .dsx-search pads with; the
       two are meant to stay equal. */
    --dsx-rail-box: 32px;
    --dsx-rail-glyph: 20px;
    --dsx-rail-inset: calc((var(--dsx-rail-box) - var(--dsx-rail-glyph)) / 2);

    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: var(--ds-2);
    height: var(--dsx-header-h);
    padding: 0 calc(var(--ds-4) - var(--dsx-rail-inset)) 0 var(--ds-5);
}

.dsx-logo {
    flex: 1 1 auto;
    min-width: 0;
    font: var(--ds-title-4);
    letter-spacing: .04em;
    line-height: 1;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--ds-blue);
    white-space: nowrap;
}

/* Collapses the panel to a rail. It is the one control that has to
   stay on screen in both states, so it keeps its box and only the
   chevrons turn round. */
.dsx-rail-toggle {
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: var(--dsx-rail-box);
    height: var(--dsx-rail-box);
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--ds-corner-radius-8px);
    color: var(--ds-text-secondary);
    cursor: pointer;
}

/* One glyph at a time. The panel-open one only appears once the
   panel is gone. */
.dsx-rail-toggle svg { width: var(--dsx-rail-glyph); height: var(--dsx-rail-glyph); }
.dsx-rail-toggle__open { display: none; }
:root.is-rail .dsx-rail-toggle__close { display: none; }
:root.is-rail .dsx-rail-toggle__open { display: block; }
.dsx-rail-toggle:hover { background: var(--ds-grey-100); color: var(--ds-text-primary); }
.dsx-rail-toggle:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* Collapsed: a rail exactly the header's height wide, holding the
   one control that undoes it. The brand, the search and the index
   are the panel's, and the panel is not here. */
:root.is-rail { --dsx-sidebar-w: var(--dsx-header-h); }
:root.is-rail .dsx-brand { justify-content: center; padding: 0; }
:root.is-rail .dsx-logo,
:root.is-rail .dsx-search,
:root.is-rail #sidebarNav { display: none; }

.dsx-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* This is the page's heading — the shell writes the component's
   name into it on every route change. Sized for a 60px bar rather
   than for a page, which is why it is not the 2rem the old
   in-page title was. */
.dsx-header__title {
    margin: 0;
    font: var(--ds-title-4);
    letter-spacing: -.01em;
    color: var(--ds-text-primary);
}

.dsx-header__spacer { flex: 1; }

/* Declared here rather than after the media queries, where its
   display:none used to win over the rule meant to reveal it — the
   drawer it opens could not be opened at all. */
.dsx-menu-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    background: transparent;
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    color: var(--ds-text-primary);
    cursor: pointer;
}

/* ----- Sidebar ----- */
/* A column: brand, search, then the index, which is the only part
   that scrolls — the first two stay put however long the list is. */
.dsx-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    display: flex;
    flex-direction: column;
    width: var(--dsx-sidebar-w);
    background: var(--ds-white);
    border-right: 1px solid var(--ds-grey-200);
    overflow: hidden;
    /* Above the header: on a narrow screen this slides over it. */
    z-index: 30;
}

/* 8px under the brand. The brand's own box is the header's height,
   so the text sits on the header's centre line and about 21px of
   that box is below the letters — the 8 is measured from the box,
   which is the only edge the two elements share. */
.dsx-search {
    position: relative;
    flex: 0 0 auto;
    padding: 8px var(--ds-4) var(--ds-4);
}

/* The Search component draws its glyph 12 from the left edge in
   Icon/Secondary, and the field's text starts clear of it. */
.dsx-search__icon {
    position: absolute;
    top: 8px;
    left: calc(var(--ds-4) + 12px);
    display: flex;
    align-items: center;
    height: 32px;
    color: var(--ds-icon-secondary);
    pointer-events: none;
}

.dsx-search__icon svg { width: 16px; height: 16px; }

/* The shell writes the index into this element, so the scroll and
   the panel's bottom padding live on it. */
#sidebarNav {
    flex: 1 1 auto;
    overflow-y: auto;
    padding-bottom: var(--ds-6);
}

.dsx-search input {
    width: 100%;
    height: 32px;
    padding: 0 var(--ds-3) 0 36px;
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    outline: none;
}
.dsx-search input::placeholder { color: var(--ds-text-secondary); }

/* A field is not a button. The library draws hover on a field
   identically to default and focus as one thing — the 1px border
   turning Outline/Brand. The 2px offset ring belongs to the things
   with a box: button, checkbox, radio, toggle. */
.dsx-search input:focus {
    border-color: var(--ds-outline-brand);
    outline: none;
}

.dsx-group { margin-bottom: var(--ds-4); }

.dsx-group__label {
    padding: 0 var(--ds-4);
    margin: 0 0 var(--ds-2);
    font: var(--ds-subtitle-3);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
}

.dsx-nav { list-style: none; margin: 0; padding: 0; }

.dsx-nav a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-2);
    padding: 6px var(--ds-4) 6px var(--ds-5);
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
    text-decoration: none;
    border-left: 2px solid transparent;
}

.dsx-nav a:hover { background: var(--ds-grey-100); }

/* The selected row is the library's own soft brand ground with the
   brand on the label, which is how the Tab and the Droplist item
   both mark the one you are on. */
.dsx-nav a[aria-current="page"] {
    font: var(--ds-subtitle-2);
    color: var(--ds-text-brand);
    background: var(--ds-bg-brand-soft);
    border-left-color: var(--ds-bg-brand);
}

.dsx-nav a.is-todo { color: var(--ds-text-disabled); cursor: default; }
.dsx-nav a.is-todo:hover { background: transparent; }

/* "Soon" is a status label and the library has one, so the nav row
   carries a real Badge. It is drawn at the component's own 24px —
   the row grows to fit it rather than the badge shrinking to fit
   the row. */
.dsx-nav .ds-badge {
    flex: 0 0 auto;
    opacity: var(--ds-opacity-disabled);
}

/* ----- Main ----- */
/* No max-width here any more: the playground band runs the full
   width of the column, and everything that is not the band carries
   the measure itself. */
/* No top padding: the band butts straight up under the header. */
.dsx-main {
    margin: var(--dsx-header-h) 0 0 var(--dsx-sidebar-w);
    padding: 0 var(--dsx-main-pad) var(--ds-7);
}

/* Flush against the header, the band's own top rule would double the
   header's underline, so it gives it up — one line, not two. */
.dsx-section--play:first-child .dsx-stage { border-top: 0; }

/* A page with no playground has no band to sit flush, so it keeps
   the space the padding used to give it. */
.dsx-main > .dsx-section:first-child:not(.dsx-section--play) {
    margin-top: var(--ds-6);
}

/* The measure is for prose — a line of text has a width past which it
   gets hard to track back to the next one. The property panel is a
   grid of controls, not a line of text, so it is not on this list: it
   fills the column the way the band above it does. */
.dsx-section__title,
.dsx-section__note,
.dsx-specimens,
.dsx-fold { max-width: var(--dsx-measure); }

.dsx-section { margin-bottom: var(--ds-7); }

.dsx-section__title {
    margin: 0 0 var(--ds-4);
    padding-bottom: var(--ds-2);
    font: var(--ds-title-4);
    color: var(--ds-text-primary);
    border-bottom: 1px solid var(--ds-outline-tertiary);
}

.dsx-section__note {
    margin: -8px 0 var(--ds-4);
    font: var(--ds-body-2);
    color: var(--ds-text-secondary);
}

/* ----- Typography scale table -----
   The specimen and its numbers on one row, so the page is read
   across rather than between a gallery and a spec. */
.dsx-table--type td { vertical-align: middle; }

.dsx-table--type td:nth-child(2) { min-width: 240px; }

.dsx-type-sample {
    display: block;
    color: var(--ds-text);
}

.dsx-type-token {
    display: block;
    margin-top: 3px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11px;
    color: var(--ds-text-secondary);
}

.dsx-type-sub {
    display: block;
    margin-top: 3px;
    font: var(--ds-body-3);
    color: var(--ds-text-secondary);
}

/* The family header. It is a single spanning cell rather than a real
   <th>, so the weight and ground are carried by the span inside it. */
.dsx-type-group {
    display: block;
    margin: 0 calc(var(--ds-4) * -1);
    padding: 4px var(--ds-4);
    background: var(--ds-bg-grayscale-a);
    font: var(--ds-subtitle-3);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
}

.dsx-type-na { color: var(--ds-grey-400); }

/* On a narrow screen the five columns squeeze rather than scroll,
   and the token wraps across three lines. Hold the table at its
   working width and let the wrapper scroll it instead. */
@media (max-width: 767px) {
    .dsx-table--type { min-width: 680px; }
}

/* ----- Folded sections -----
   Below the playground the page turns into a list of closed rows.
   The reader chooses what to open; nothing arrives uninvited. */
.dsx-fold {
    border-bottom: 1px solid var(--ds-grey-200);
}

.dsx-fold + .dsx-fold { margin-top: 0; }

.dsx-fold__summary {
    display: flex;
    align-items: center;
    gap: var(--ds-2);
    padding: var(--ds-3) 0;
    font: var(--ds-subtitle-1);
    color: var(--ds-text-primary);
    cursor: pointer;
    list-style: none;
    user-select: none;
}

/* Safari draws its own triangle unless both of these are set. */
.dsx-fold__summary::-webkit-details-marker { display: none; }

.dsx-fold__summary::before {
    content: '';
    flex: none;
    width: 7px;
    height: 7px;
    margin-left: 2px;
    border-right: 1.5px solid var(--ds-grey-400);
    border-bottom: 1.5px solid var(--ds-grey-400);
    transform: rotate(-45deg) translate(-1px, -1px);
    transition: transform .15s ease;
}

.dsx-fold[open] > .dsx-fold__summary::before {
    transform: rotate(45deg) translate(-1px, -1px);
}

.dsx-fold__summary:hover { color: var(--ds-blue); }
.dsx-fold__summary:hover::before { border-color: var(--ds-blue); }

.dsx-fold__summary:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
    border-radius: var(--ds-corner-radius-4px);
}

.dsx-fold__body {
    padding: var(--ds-1) 0 var(--ds-6);
}

/* The note leads the body here, so it has no title above to pull up to. */
.dsx-fold__body > .dsx-section__note:first-child {
    margin-top: 0;
}

/* ----- Playground: stage + controls ----- */
/* Stage on top, controls under it. The panel was a 280px column
   beside the stage, which squeezed the specimen on the very pages
   whose specimens are widest. */
.dsx-play {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    /* The same as the gutter: the panel sits the same distance below
       the band as it does inside the column. */
    gap: var(--ds-4);
    align-items: start;
}

/* A band rather than a card: it cancels the page gutter on both
   sides and pads it straight back, so the band touches the sidebar
   and the window while its contents stay on the page's own column.
   Ruled top and bottom only — there are no side edges to draw.

   Plain white, no grid. The grid read as texture behind every
   specimen, and a component cannot be judged against a pattern —
   least of all once there is a light and a dark surface to compare
   it on. */
.dsx-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    /* A grid item's min-width is auto, so without this any specimen
       wider than the window drags the whole page sideways with it
       instead of being clipped or scrolled inside its own box. */
    min-width: 0;
    /* At least 40% of the window, so the specimen has room to be
       looked at rather than glanced past. max() keeps the old 240px
       as a floor for short windows, where 40% is less than that. */
    min-height: max(240px, 40vh);
    margin: 0 calc(var(--dsx-main-pad) * -1);
    padding: var(--ds-6) var(--dsx-main-pad);
    background: var(--dsx-stage-bg);
    border-top: 1px solid var(--dsx-stage-line);
    border-bottom: 1px solid var(--dsx-stage-line);
}

/* Centred, at the component's own width rather than the window's.
   Contents are left-aligned inside it: a field with its label
   centred over it stops looking like a form. */
.dsx-stage__inner {
    width: 100%;
    max-width: var(--dsx-specimen-w);
    min-width: 0;
}

/* Page-width components — a table, a modal, an alert bar — take the
   whole band. This is what playWide has always meant; until now it
   set a class nothing read. */
.dsx-play--wide .dsx-stage__inner { max-width: none; }

/* The panel packs by kind rather than on a fixed grid. A grid gave
   every column the height of the tallest control in its row, and one
   control — the state list, eight or nine options — is far taller
   than the rest, so short controls sat above a screen of dead space.
   Flex, and each control as wide as it needs, closes that up.

   The order is choices, then colour, then the words, then the
   switches. Declaration order holds inside each group. */
/* Two columns and three groups. On the left, what the component can
   be and what it can carry; on the right, what it says. Copy is not
   a variant, and on the Table it is 8 of the 14 controls — beside
   the others it reads as a separate job, which it is.

   Group headings sit in a rail down the left of their own group, so
   they cost no vertical height. */
/* Bands, not columns. The panel was split by kind — structure left,
   copy right — at a fixed 360px. Kind is not volume, so the two sides
   almost never balanced: Colour ran 744px of controls against 77px of
   copy, File upload ran 114 against 205. Measured across all twenty
   pages, three landed near even and the rest were lopsided one way or
   the other.

   Stacked bands cannot be lopsided. Each one runs the full width, the
   reading order is Variants, Slots, Content, and a rule sets the copy
   apart from the structure above it. */
.dsx-controls {
    display: flex;
    flex-direction: column;
    gap: var(--ds-4);
    padding: var(--ds-4) var(--ds-5);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

/* Properties and Content: the panel's only two headings, set the
   same so they read as one level. */
.dsx-controls h3 {
    margin: 0;
    font: var(--ds-subtitle-3);
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
}

.dsx-controls__col {
    display: flex;
    flex-direction: column;
    gap: var(--ds-4);
    min-width: 0;
}

/* Stacked, copy is the last band and the rule above it marks the
   change from what the component is to what it says. */
/* The rule runs the full width of the card rather than stopping at
   its padding, so the copy reads as its own section of the card and
   not as a line drawn inside the first one. */
.dsx-controls__col--aside {
    margin-inline: calc(var(--ds-5) * -1);
    padding: var(--ds-4) var(--ds-5) 0;
    border-top: 1px solid var(--ds-grey-200);
}

/* ---------- Desktop: copy takes a column of its own ----------
   Wide enough and the two stop competing for the same rows: the
   variants and slots keep the left, copy stands in a column down the
   right with one field per line. Stacked fields read as a list you
   work down; the same fields in a row read as a toolbar. */

@media (min-width: 1024px) {
    .dsx-controls {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
        align-items: start;
        column-gap: var(--ds-5);
        row-gap: var(--ds-4);
    }

    .dsx-controls > h3 { grid-column: 1; grid-row: 1; }
    .dsx-controls__col:not(.dsx-controls__col--aside) { grid-column: 1; grid-row: 2; }

    /* The rule moves from above it to beside it, and runs the whole
       height of the card: the column spans both rows and reaches out
       through the card's top and bottom padding. It is a subgrid, so
       Content sits on the same line as Properties and its fields on
       the same line as the controls beside them. Per the
       subgrid spec, the negative margin and the padding cancel on the
       edge tracks, so nothing inside moves. */
    .dsx-controls__col--aside {
        grid-column: 2;
        grid-row: 1 / span 2;
        align-self: stretch;
        display: grid;
        grid-template-rows: subgrid;
        row-gap: var(--ds-4);
        margin: calc(var(--ds-4) * -1) 0;
        padding: var(--ds-4) 0 var(--ds-4) var(--ds-5);
        border-top: 0;
        border-left: 1px solid var(--ds-grey-200);
    }

    .dsx-controls__col--aside > h3 { grid-row: 1; }

    /* One field per line. align-self: the row is as tall as the
       left column, and the fields belong at the top of it, not
       spread down it. */
    .dsx-controls__col--aside > .dsx-controls__body {
        grid-row: 2;
        align-self: start;
        grid-template-columns: minmax(0, 1fr);
        gap: var(--ds-3);
    }
}

/* One rhythm for every control in the panel, whatever kind it is.
   The old layout gave each kind its own flex basis, so a segment, a
   text field and a checkbox row all took different widths and nothing
   lined up with anything — which is what read as disorder. On a grid
   they share column edges. */
.dsx-controls__body {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: var(--ds-4);
    align-items: start;
}

.dsx-control { margin-bottom: 0; min-width: 0; }

/* The three that are rows rather than cells. */
.dsx-control--swatches,
.dsx-control--wide { grid-column: 1 / -1; }

/* ---------- Exclusive choices with more than two options ----------
   Read down, one per line. Horizontal was fine for two and became a
   tab bar at eight. */

.dsx-radios {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* The library's Radio does the drawing; the panel only says where
   it sits. Everything here used to be a second radio — a native
   input tinted with accent-color — standing two rows above the page
   that documents the real one. */
.dsx-radio {
    display: flex;
    padding: 3px 0;
}

/* An author display beats the browser's [hidden] rule, so an option
   the config has taken out of play has to be hidden here too. */
.dsx-radio[hidden] { display: none; }

/* The one thing the panel adds: the row you picked is named in the
   brand, the way the sidebar names the page you are on. */
.dsx-radio:has(.ds-radio__input:checked) .ds-radio__label {
    font: var(--ds-subtitle-2);
    color: var(--ds-text-brand);
}

/* A sentence under the label rather than in place of it. */
.dsx-control__hint {
    margin: -2px 0 var(--ds-3);
    font: var(--ds-body-3);
    color: var(--ds-text-secondary);
}

/* Scoped to the text control's own <label for>. It used to match
   every direct-child label in the panel, which now includes the
   Checkbox's — and display:block on a .ds-check collapses the box
   it draws. */
.dsx-control--text > label,
.dsx-control__label {
    display: block;
    margin-bottom: 6px;
    font: var(--ds-subtitle-3);
    color: var(--ds-text-primary);
}

.dsx-control input[type="text"] {
    width: 100%;
    height: 32px;
    padding: 0 var(--ds-3);
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    outline: none;
}
/* Focus is the border turning Outline/Brand and nothing else, which
   is what the Input field itself does two rows down the page. */
.dsx-control input[type="text"]:focus {
    border-color: var(--ds-outline-brand);
    outline: none;
}

/* Segmented control */
/* As wide as its options, not as wide as its cell. The grid gives a
   segment room enough that its buttons never wrap; without this, four
   one-digit options like Rows would stretch that whole allowance and
   read as four enormous buttons. Cells still line up — only the track
   inside them stops stretching. */
.dsx-seg {
    display: flex;
    /* Buttons no longer stretch, so on a phone they have to be allowed
       to wrap — otherwise a four-option track just overflows its cell. */
    flex-wrap: wrap;
    width: fit-content;
    max-width: 100%;
    padding: 2px;
    background: var(--ds-bg-grayscale-b);
    border-radius: var(--ds-corner-radius-8px);
}

/* A stacked list is a column of full-width rows, so it keeps the
   cell's width. */

.dsx-seg button {
    flex: 0 0 auto;
    padding: 5px 10px;
    font: var(--ds-button-3);
    color: var(--ds-text-secondary);
    background: transparent;
    border: 0;
    border-radius: var(--ds-corner-radius-6px);
    cursor: pointer;
}
.dsx-seg button:hover { color: var(--ds-text-primary); }
/* The picked segment carries the same pair the Tabs pill does. */
.dsx-seg button[aria-pressed="true"] {
    background: var(--ds-bg-brand-soft);
    color: var(--ds-text-brand);
}
.dsx-seg button:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 1px;
}

/* Stacked vertically this made sense in a 280px column. Under the
   stage there is room to lay the options out in a line. */

/* ---------- Wide playground ----------
   Opt-in per component (def.playWide). The stage sits above a
   full-width control panel, which is what the Colour page's three
   parallel pickers need — they do not fit the 280px column. */
/* ---------- The role picker ----------
   One row per role. Four side-by-side columns wrote the same ten
   option names three times over and stood 600px tall; the table
   writes them once, into a select, and stacks the roles so they read
   against one another. */
.dsx-swatchtable {
    width: 100%;
    border-collapse: collapse;
}

.dsx-swatchtable th,
.dsx-swatchtable td {
    padding: 5px 0;
    text-align: left;
    vertical-align: middle;
}

.dsx-swatchtable thead th {
    padding-bottom: 5px;
    font: var(--ds-subtitle-3);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
    border-bottom: 1px solid var(--ds-outline-tertiary);
}

/* The role name and the hex are as wide as they need to be; the
   picker takes what is left, so the table fills whatever the panel
   gives it instead of stopping at a stated width. */
.dsx-swatchtable tbody th {
    width: 1%;
    padding-right: var(--ds-5);
    white-space: nowrap;
    font: var(--ds-body-2);
}

.dsx-swatchtable__pick { width: auto; }

/* Below this the role name wraps rather than squeezing the picker. */
@media (max-width: 520px) {
    .dsx-swatchtable tbody th { white-space: normal; padding-right: var(--ds-3); }
}

.dsx-swatchtable tbody tr + tr th,
.dsx-swatchtable tbody tr + tr td { border-top: 1px solid var(--ds-grey-100); }

/* The dot and its select ride together in the cell. */
.dsx-swatchtable td > * { vertical-align: middle; }

/* Off keeps the row in place and disables what is in it. */
.dsx-swatchtable tr[data-off] th,
.dsx-swatchtable tr[data-off] td { opacity: .45; }

/* Inside the trigger now, beside the value it belongs to, rather
   than loose in the cell next to the control. */
.dsx-swatchdot {
    display: block;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    border-radius: var(--ds-corner-radius-4px);
    /* A hairline so the near-white values are still visible as
       chips — the picker has to show what the ramp cannot. */
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12);
}

/* ---------- The picker ----------
   A native <select> draws its popup with the operating system, and
   nothing in a stylesheet reaches it — so the one control on the
   page that could not be made to look like this library was the one
   choosing its colours. The trigger is shaped like the Dropdown at
   Small and the panel is the Droplist. */

.dsx-picker {
    position: relative;
    display: inline-block;
    width: calc(100% - 24px);
    min-width: 180px;
    max-width: 340px;
    vertical-align: middle;
}

.dsx-picker__trigger {
    display: flex;
    align-items: center;
    gap: var(--ds-2);
    width: 100%;
    height: 32px;
    padding: 0 var(--ds-2) 0 var(--ds-3);
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
    text-align: left;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    cursor: pointer;
}

.dsx-picker__value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The hex used to be a column of its own. It belongs to the value,
   so it travels with it — here on the trigger and again on every
   row of the panel. */
.dsx-picker__hex {
    flex: 0 0 auto;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    color: var(--ds-text-secondary);
}

.dsx-picker__chevron {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    color: var(--ds-icon-secondary);
}

.dsx-picker__chevron svg { width: 16px; height: 16px; }

/* Focus is the border turning Outline/Brand, the same single thing
   the Input field does. */
.dsx-picker__trigger:focus-visible {
    border-color: var(--ds-outline-brand);
    outline: none;
}

.dsx-picker__trigger[aria-expanded="true"] { border-color: var(--ds-outline-brand); }
.dsx-picker__trigger[aria-expanded="true"] .dsx-picker__chevron { transform: scaleY(-1); }

.dsx-picker__trigger:disabled { cursor: not-allowed; }

/* 4px under the trigger — the gap the Dropdown's own panel is drawn
   at, which the frame does not measure either. */
.dsx-picker__panel {
    position: absolute;
    z-index: 40;
    top: calc(100% + 4px);
    left: 0;
    min-width: 100%;
}

/* The Droplist is drawn at a fixed 328 in its own frame. Here it
   belongs to the control above it. */
.dsx-picker__panel .ds-droplist {
    width: 100%;
    /* No footer, so the 8px the frame puts above one is owed back. */
    padding-bottom: 8px;
}

/* Checkbox row */
/* Toggles run along their row instead of stacking. */
.dsx-control--checks {
    display: flex;
    flex-direction: column;
    gap: 0;
    min-width: 0;
}

/* Slots read down too. They stay checkboxes — you can have any
   number of them at once, which is what separates them from the
   choices above. The box itself is the library's Checkbox. */
.dsx-check {
    display: flex;
    padding: 3px 0;
}

/* ----- Code block ----- */
.dsx-code {
    position: relative;
    margin: 0;
    padding: var(--ds-4);
    padding-right: 84px;
    overflow-x: auto;
    background: var(--ds-bg-inverse);
    border-radius: var(--ds-corner-radius-8px);
}

.dsx-code code {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    font-size: 13px;
    line-height: 1.7;
    color: var(--ds-text-inverse);
    white-space: pre;
}

.dsx-copy {
    position: absolute;
    top: var(--ds-3);
    right: var(--ds-3);
    padding: 5px 10px;
    font: var(--ds-button-3);
    color: var(--ds-text-inverse);
    background: rgba(255, 255, 255, .1);
    border: 1px solid rgba(255, 255, 255, .18);
    border-radius: var(--ds-corner-radius-6px);
    cursor: pointer;
}
.dsx-copy:hover { background: rgba(255, 255, 255, .18); }

/* ----- Specimen grids (variant galleries) ----- */
.dsx-specimens {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
    gap: var(--ds-5);
}

.dsx-specimen {
    padding: var(--ds-4);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

.dsx-specimen__name {
    margin: 0 0 var(--ds-3);
    font: var(--ds-subtitle-3);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
}

/* ----- Spec table ----- */
.dsx-table-wrap {
    overflow-x: auto;
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

.dsx-table {
    width: 100%;
    border-collapse: collapse;
    background: var(--ds-bg-background);
    font: var(--ds-body-2);
}

.dsx-table th,
.dsx-table td {
    padding: 10px var(--ds-4);
    text-align: left;
    border-bottom: 1px solid var(--ds-outline-tertiary);
    vertical-align: top;
}

.dsx-table th {
    font: var(--ds-subtitle-3);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ds-text-secondary);
    background: var(--ds-bg-grayscale-a);
}

.dsx-table tr:last-child td { border-bottom: 0; }

.dsx-table code,
.dsx-inline-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 13px;
    padding: 1px 5px;
    background: var(--ds-bg-brand-soft);
    color: var(--ds-text-brand);
    border-radius: var(--ds-corner-radius-4px);
}

/* ----- Responsive ----- */

@media (max-width: 1024px) {
    .dsx-play { grid-template-columns: 1fr; }
}

@media (max-width: 768px) {
    /* The panel is already a drawer down here, so there is nothing
       for the rail to do — and the rail's own width would push the
       drawer back on screen. */
    :root,
    :root.is-rail { --dsx-sidebar-w: 0px; }
    .dsx-rail-toggle { display: none; }
    :root.is-rail .dsx-logo,
    :root.is-rail .dsx-search,
    :root.is-rail #sidebarNav { display: revert; }
    :root.is-rail .dsx-brand { justify-content: flex-start; padding: 0 var(--ds-5); }
    .dsx-sidebar {
        transform: translateX(-100%);
        transition: transform .22s ease;
        width: 264px;
        box-shadow: var(--ds-elevation-alert);
    }
    .dsx-sidebar.is-open { transform: none; }
    .dsx-menu-toggle { display: inline-flex; }
    .dsx-main { margin-left: 0; }
}

/* ============================================================
   COMPONENT: Input / Standard
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame
   "✅ Input / ✅ Standard" (node 1:26717).

   Token names below mirror the Figma variable names exactly, so a
   change in the library maps to one line here. Two types
   (adjacent label / floating label) x two sizes (Large / Small)
   x eight states.
   ============================================================ */

:root {
    /* Text */
    --ds-text-primary: #3D4C66;
    --ds-text-secondary: #758696;
    --ds-text-brand: #2797E8;
    --ds-text-error: #FF525F;

    /* Icon */
    --ds-icon-secondary: #758696;
    --ds-icon-error: #FF525F;

    /* Outline */
    --ds-outline-tertiary: #E5E0EB;
    --ds-outline-primary: #3D4C66;
    --ds-outline-brand: #2797E8;
    --ds-outline-error: #FF525F;

    /* Bg */
    --ds-bg-background: #FFFFFF;
    --ds-bg-grayscale-a: #FAF9FC;

    /* corner-radius */
    --ds-corner-radius-8px: 8px;

    /* The Figma file names this family two ways and neither is the
       family: the Input frames report "Final", a placeholder, and the
       Typography frame reports "Poppin". It is Poppins. */
    --ds-font: "Poppins", sans-serif;

    /* Figma expresses the disabled treatment as a 40% layer opacity */
    --ds-opacity-disabled: 0.4;
}

/* ---------- Field ---------- */

.ds-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
    font-family: var(--ds-font);
}

/* ---------- Adjacent label ---------- */

.ds-field__label {
    display: flex;
    align-items: center;
    margin: 0;
    line-height: 1.5;
    color: var(--ds-text-secondary);
}

.ds-field--lg .ds-field__label { font-size: 14px; }  /* Body 2 */
.ds-field--sm .ds-field__label { font-size: 12px; }  /* Body 3 */

/* ---------- Control ---------- */

.ds-field__control {
    position: relative;
    display: flex;
    box-sizing: border-box;
    align-items: center;
    width: 100%;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

/* Figma draws the 1px stroke INSIDE the frame, so it does not add to the
   height: Large measures 44px and Small 32px overall. The padding below is
   therefore the Figma value minus the border, which puts the content at
   exactly 10px/12px (Large) and 4px/8px (Small) from the outer edge. */
.ds-field--lg .ds-field__control { gap: 12px; padding: 9px 11px; }
.ds-field--sm .ds-field__control { gap: 8px; padding: 3px 7px; height: 32px; }

/* Leading icon + text share one group; the 40% disabled treatment is
   applied to this group, matching the Figma layer structure. */
.ds-field__body {
    display: flex;
    flex: 1 0 0;
    align-items: center;
    gap: 4px;
    min-width: 0;
}

/* ---------- Icons: Bags-Suitcase, 24px box at every size ---------- */

.ds-field__icon {
    position: relative;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    color: var(--ds-icon-secondary);
}

.ds-field__icon svg {
    display: block;
    width: 24px;
    height: 24px;
}

/* ---------- Input ---------- */

.ds-input {
    flex: 1 0 0;
    width: 100%;
    min-width: 0;
    margin: 0;
    padding: 0;
    font-family: inherit;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ds-text-primary);
    text-overflow: ellipsis;
    background: transparent;
    border: 0;
    border-radius: 0;
    outline: none;
    appearance: none;
}

/* Figma draws the resting text at 40% of Text/Primary */
.ds-input::placeholder {
    color: var(--ds-text-primary);
    opacity: var(--ds-opacity-disabled);
}

.ds-field--lg .ds-input { font-size: 16px; }  /* Body 1 */
.ds-field--sm .ds-input { font-size: 14px; }  /* Body 2 */

/* ---------- Suffix ---------- */

.ds-field__suffix {
    flex: 0 0 auto;
    line-height: 1.5;
    color: var(--ds-text-secondary);
    white-space: nowrap;
}

.ds-field--lg .ds-field__suffix { font-size: 16px; }
.ds-field--sm .ds-field__suffix { font-size: 14px; }

/* ---------- Caret ----------
   Static specimens only. A live field uses the browser's own caret,
   which is what this hairline stands in for. */

.ds-field__caret {
    flex: 0 0 auto;
    width: 1px;
    background: var(--ds-outline-primary);
}

.ds-field--lg .ds-field__caret { height: 24px; }
.ds-field--sm .ds-field__caret { height: 20px; }

/* A caret at the START of a field is an insertion point sitting ON the
   text origin, not a spacer in front of it — every frame draws the
   resting text beginning at the same x as the caret. The negative
   margin cancels the caret's own 1px and the body's 4px gap, so the
   text stays where it belongs instead of being shunted 5px right. */
.ds-field__caret--start { margin-right: -5px; }

/* ---------- Help text ----------
   Shared with Search, whose frame draws this row on exactly these
   coordinates — the size selectors below carry .ds-search--lg / --sm
   alongside .ds-field--lg / --sm so the geometry has one home. */

.ds-field__help {
    display: flex;
    align-items: center;
    gap: 4px;
    width: 100%;
    margin: 0;  /* the row is a <p>; its UA margin would break the 4px gap */
}

/* The glyph does not fill its box: Figma insets it 12.5% left/right
   and 8.33% top, so both boxes are pinned explicitly. */
.ds-field__help-icon {
    position: relative;
    flex: 0 0 auto;
    color: var(--ds-icon-secondary);
}

.ds-field__help-icon svg { position: absolute; display: block; }

.ds-field--lg .ds-field__help-icon,
.ds-search--lg .ds-field__help-icon { width: 20px; height: 20px; }
.ds-field--lg .ds-field__help-icon svg,
.ds-search--lg .ds-field__help-icon svg {
    left: 2.5px;
    top: 1.6667px;
    width: 15.0009px;
    height: 13.8339px;
}

.ds-field--sm .ds-field__help-icon,
.ds-search--sm .ds-field__help-icon { width: 16px; height: 16px; }
.ds-field--sm .ds-field__help-icon svg,
.ds-search--sm .ds-field__help-icon svg {
    left: 2px;
    top: 1.3333px;
    width: 12.0007px;
    height: 11.1671px;
}

.ds-field__help-text {
    flex: 1 0 0;
    min-width: 0;
    line-height: 1.5;
    color: var(--ds-text-secondary);
}

.ds-field--lg .ds-field__help-text,
.ds-search--lg .ds-field__help-text { font-size: 12px; }  /* Body 3 */
.ds-field--sm .ds-field__help-text,
.ds-search--sm .ds-field__help-text { font-size: 10px; }  /* Description */

/* ============================================================
   Floating label
   ============================================================ */

.ds-field__float {
    position: absolute;
    display: flex;
    align-items: center;
    line-height: 1.5;
    color: var(--ds-text-secondary);
    white-space: nowrap;
    pointer-events: none;
    transition: top .15s ease, left .15s ease, font-size .15s ease, color .15s ease;
}

/* Resting: sits in the text slot — past the padding, the 24px
   leading icon and the 4px gap.

   An absolutely positioned box is laid out against its containing
   block's PADDING box, so every offset here is the Figma value from
   the outer edge minus the 1px border. Getting that wrong is what
   put the label 1px right and 1px low until the Dropdown export
   made the error measurable. */
.ds-field--lg .ds-field__float {
    left: 39px;   /* 40px from the outer edge */
    top: 50%;
    font-size: 16px;
    transform: translateY(-50%);
}

.ds-field--sm .ds-field__float {
    left: 35px;   /* 36px from the outer edge */
    top: 50%;
    font-size: 14px;
    transform: translateY(-50%);
}

.ds-field--lg.ds-field--no-leading .ds-field__float { left: 11px; }
.ds-field--sm.ds-field--no-leading .ds-field__float { left: 7px; }

/* Floated: notched into the top border on a background-coloured chip */
.ds-field--lg.is-floated .ds-field__float,
.ds-field--floating.ds-field--lg .ds-field__control:focus-within .ds-field__float,
.ds-field--floating.ds-field--lg .ds-field__control:has(input.ds-input:not(:placeholder-shown)) .ds-field__float {
    left: 9px;    /* 10px from the outer edge */
    top: -10px;   /* -9px from the outer edge */
    padding: 0 2px;
    font-size: 12px;
    background: var(--ds-bg-background);
    transform: none;
}

.ds-field--sm.is-floated .ds-field__float,
.ds-field--floating.ds-field--sm .ds-field__control:focus-within .ds-field__float,
.ds-field--floating.ds-field--sm .ds-field__control:has(input.ds-input:not(:placeholder-shown)) .ds-field__float {
    left: 5px;    /* 6px from the outer edge */
    top: -9px;    /* -8px from the outer edge */
    padding: 0 2px;
    font-size: 10px;
    background: var(--ds-bg-background);
    transform: none;
}

/* While the label still rests in the text slot it would collide with
   the placeholder, so the hint only appears once the label lifts. */
.ds-field--floating .ds-input::placeholder { color: transparent; }

.ds-field--floating .ds-field__control:focus-within .ds-input::placeholder {
    color: var(--ds-text-primary);
    opacity: var(--ds-opacity-disabled);
}

/* A floated label takes the brand colour while the field has focus */
.ds-field--floating .ds-field__control:focus-within .ds-field__float {
    color: var(--ds-text-brand);
}

/* ============================================================
   States
   ============================================================ */

/* Hover — the library draws hover identically to default
   (Outline/Tertiary). Kept as its own rule so adding a hover
   outline later is a one-line change. */
.ds-field--hover .ds-field__control { border-color: var(--ds-outline-tertiary); }

/* Focus + Typing — Outline/Brand */
.ds-field__control:focus-within,
.ds-field--focus .ds-field__control,
.ds-field--typing .ds-field__control {
    border-color: var(--ds-outline-brand);
}

.ds-field--focus .ds-field__float,
.ds-field--typing .ds-field__float {
    color: var(--ds-text-brand);
}

/* Entered — back to Outline/Tertiary, value at full strength */
.ds-field--entered .ds-field__control { border-color: var(--ds-outline-tertiary); }

/* Error — Outline/Error, and the help row turns red */
.ds-field--error .ds-field__control,
.ds-field--error .ds-field__control:focus-within {
    border-color: var(--ds-outline-error);
}

.ds-field--error .ds-field__help-icon { color: var(--ds-icon-error); }
.ds-field--error .ds-field__help-text { color: var(--ds-text-error); }
/* Only a floated label turns red; while it still rests inside the
   field it stays Text/Secondary, as Error-Empty is drawn. */
.ds-field--error.is-floated .ds-field__float,
.ds-field--error .ds-field__control:focus-within .ds-field__float,
.ds-field--error .ds-field__control:has(input.ds-input:not(:placeholder-shown)) .ds-field__float {
    color: var(--ds-text-error);
}

/* Disabled — Bg/Grayscale A behind the field, leading icon and text
   at 40%. The trailing icon sits outside that group in the Figma
   structure, so it stays at full strength. */
.ds-field--disabled .ds-field__control {
    background: var(--ds-bg-grayscale-a);
    border-color: var(--ds-outline-tertiary);
}

.ds-field--disabled .ds-field__body { opacity: var(--ds-opacity-disabled); }
.ds-field--disabled .ds-field__help { opacity: var(--ds-opacity-disabled); }
.ds-field--disabled .ds-field__suffix { opacity: var(--ds-opacity-disabled); }

.ds-input:disabled {
    color: var(--ds-text-primary);
    -webkit-text-fill-color: var(--ds-text-primary);
    cursor: not-allowed;
}

/* Read-only — same grey ground, value stays legible */
.ds-field--readonly .ds-field__control {
    background: var(--ds-bg-grayscale-a);
    border-color: var(--ds-outline-tertiary);
}

.ds-field--readonly .ds-field__control:focus-within {
    border-color: var(--ds-outline-tertiary);
}

/* The floating variant renders its read-only value in Text/Secondary
   where the adjacent variant uses Text/Primary — mirrored from Figma. */
.ds-field--floating.ds-field--readonly .ds-input { color: var(--ds-text-secondary); }

/* ---------- Static value slot ----------
   Specimen galleries render the value as text rather than a live
   input, so the caret can sit tight against it the way the frame
   draws it. */

.ds-input--static {
    display: block;
    flex: 1 0 0;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.ds-field--lg .ds-input--static { min-height: 24px; }
.ds-field--sm .ds-input--static { min-height: 21px; }

.ds-input--static.is-resting { opacity: var(--ds-opacity-disabled); }
.ds-input--static.is-hugged { flex: 0 1 auto; width: auto; }

/* Provenance line at the foot of a component page */
.dsx-source {
    margin: 0;
    padding-top: var(--ds-5);
    border-top: 1px solid var(--ds-outline-tertiary);
    font: var(--ds-body-3);
    color: var(--ds-text-secondary);
}

/* Callout — used to mark a component whose spec is not fully verified */
/* ============================================================
   COMPONENT: Input / Password
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame
   "Input / Password" (SVG export, 914x1303).

   The box is Input / Standard with the leading icon dropped, so
   the label, control, help row, caret and every state colour come
   from that block above and are not repeated here. Two things are
   new: the masked value — a row of 6px dots the frame draws in
   place of the text — and the trailing reveal toggle.
   ============================================================ */

/* ---------- Reveal toggle ----------
   A real <button> in the trailing icon slot. Same 24px box as
   .ds-field__icon at both sizes; the button chrome is stripped so
   the glyph lands on exactly the pixel the frame draws it on. */

.ds-field__reveal {
    position: relative;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    padding: 0;
    color: var(--ds-icon-secondary);
    background: transparent;
    border: 0;
    border-radius: 4px;
    cursor: pointer;
}

.ds-field__reveal svg {
    display: block;
    width: 24px;
    height: 24px;
}

.ds-field__reveal:disabled { cursor: not-allowed; }

/* Nothing in the frame gives this button a focus state of its own,
   so the browser default is kept rather than removed — see the
   Design review. */
.ds-field__reveal:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 1px;
}

/* ---------- Masked value ----------
   Figma draws the mask as ten 6px circles on a 10px pitch, which
   puts them 4px apart — the same gap .ds-field__body already keeps
   between the value and the caret, so the row is a flex child and
   not a set of absolute positions.

   Static specimens only. A live field is <input type="password">
   and masks with the platform's own bullet, which no stylesheet
   can resize; that split is finding 5 in the review. */

.ds-mask {
    display: flex;
    flex: 0 1 auto;
    align-items: center;
    gap: 4px;
    min-width: 0;
    color: var(--ds-text-primary);
}

.ds-field--lg .ds-mask { min-height: 24px; }
.ds-field--sm .ds-mask { min-height: 21px; }

.ds-mask__dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    background: currentColor;
    border-radius: 50%;
}

/* ---------- As drawn ---------- */

/* The frame leaves the help row at full strength on Disabled, where
   Input / Standard dims it to 40%. Dropdown and Multi-select already
   do what this frame does, so Input / Standard is the outlier —
   finding 8, left as drawn because the fix belongs over there. */
.ds-field--password.ds-field--disabled .ds-field__help { opacity: 1; }

/* Finding 6: the Small caret is drawn 24px on Typing and 20px on
   Focus, where Large is a consistent 24px. Reproduced on the Typing
   rows only; Fidelity puts every Small caret back to 20px. */
.ds-field--password.ds-field--sm.ds-field--caret-tall .ds-field__caret { height: 24px; }

/* The masked Error row keeps a Text/Secondary hint while the
   revealed one turns it red. Reproduced so the two can be read side
   by side; Fidelity turns it red on both — finding 1. */
.ds-field--password.ds-field--error.ds-field--help-neutral .ds-field__help-icon {
    color: var(--ds-icon-secondary);
}

.ds-field--password.ds-field--error.ds-field--help-neutral .ds-field__help-text {
    color: var(--ds-text-secondary);
}

/* Finding 9: the floating frame stands Read-only on Bg/Background
   while the adjacent frame stands it on Bg/Grayscale A — the same
   state on two grounds. Reproduced as drawn; Fidelity puts both on
   Bg/Grayscale A. The mask goes Text/Secondary either way, which is
   the rule Input / Standard already applies to a floating read-only
   value and is left alone. */
.ds-field--password.ds-field--floating.ds-field--readonly .ds-field__control {
    background: var(--ds-bg-background);
}

.ds-field--password.ds-field--floating.ds-field--readonly .ds-mask {
    color: var(--ds-text-secondary);
}

/* ---------- Proposed corrections ----------
   Off by default. Each rule is numbered against the Design review
   table on the component page and switched on together by the
   Fidelity control, which adds .ds-field--proposed. */

/* 2 — Hover answers the pointer, one step darker on the outline.
   Outline/Secondary is already in the library, from Text area. */
.ds-field--password.ds-field--proposed.ds-field--hover .ds-field__control,
.ds-field--password.ds-field--proposed .ds-field__control:hover {
    border-color: var(--ds-outline-secondary);
}

/* 3 — Focus carries a halo as well as the 1px border: 20% of
   Outline/Brand, not a new hue. After the hover rule, so a hovered
   and focused field still reads as focused. */
.ds-field--password.ds-field--proposed.ds-field--focus .ds-field__control,
.ds-field--password.ds-field--proposed.ds-field--typing .ds-field__control,
.ds-field--password.ds-field--proposed .ds-field__control:focus-within {
    border-color: var(--ds-outline-brand);
    box-shadow: 0 0 0 3px rgba(39, 151, 232, .2);
}

.ds-field--password.ds-field--proposed.ds-field--error.ds-field--focus .ds-field__control,
.ds-field--password.ds-field--proposed.ds-field--error .ds-field__control:focus-within {
    border-color: var(--ds-outline-error);
    box-shadow: 0 0 0 3px rgba(255, 82, 95, .2);
}

/* 4 — The toggle survives Disabled and Read-only. It is rendered by
   the component for this fidelity only; the disabled instance keeps
   the 40% treatment the rest of the body carries. */
.ds-field--password.ds-field--proposed.ds-field--disabled .ds-field__reveal {
    opacity: var(--ds-opacity-disabled);
}

/* 9 — One ground for Read-only, whichever label treatment is in
   use. Bg/Grayscale A is the one that says "not yours to edit". */
.ds-field--password.ds-field--proposed.ds-field--floating.ds-field--readonly .ds-field__control {
    background: var(--ds-bg-grayscale-a);
}

/* ============================================================
   COMPONENT: Input / Code
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame
   "✅ Input / ✅ Code" (node 1:27814).

   PARTIALLY DERIVED. The Figma symbol dimensions were read
   (Large 304x72, Small 212x56) but the tool-call quota ran out
   before the inner spec could be fetched. Box count, box size and
   gaps below are the only values that satisfy those dimensions;
   colours and type sizes are inherited from Input / Standard,
   which shares this frame's state names. Verify before shipping.
   ============================================================ */

.ds-code {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: max-content;
    font-family: var(--ds-font);
}

.ds-code__boxes { display: flex; }

.ds-code--lg .ds-code__boxes { gap: 8px; }  /* 6x44 + 5x8  = 304 */
.ds-code--sm .ds-code__boxes { gap: 4px; }  /* 6x32 + 5x4  = 212 */

.ds-code__box {
    box-sizing: border-box;
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    font-family: inherit;
    font-weight: 400;
    line-height: 1.5;
    text-align: center;
    color: var(--ds-text-primary);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    outline: none;
    appearance: none;
    transition: border-color .15s ease, background-color .15s ease;
}

.ds-code--lg .ds-code__box { width: 44px; height: 44px; font-size: 16px; }
.ds-code--sm .ds-code__box { width: 32px; height: 32px; font-size: 14px; }

/* Static specimens draw the digit and caret rather than using an input */
.ds-code__box--static {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------- States ---------- */

/* Focus / Typing — only the active box takes the brand outline */
.ds-code__box:focus,
.ds-code--focus .ds-code__box.is-active,
.ds-code--typing .ds-code__box.is-active {
    border-color: var(--ds-outline-brand);
}

/* Error — every box carries the error outline */
.ds-code--error .ds-code__box { border-color: var(--ds-outline-error); }
.ds-code--error .ds-field__help-icon { color: var(--ds-icon-error); }
.ds-code--error .ds-field__help-text { color: var(--ds-text-error); }

/* Disabled — grey ground, digit at 40%. The standard input dims a whole
   layer, but background and text share one element here, so the 40%
   lands on the text colour instead. */
.ds-code--disabled .ds-code__box {
    background: var(--ds-bg-grayscale-a);
    color: color-mix(in srgb, var(--ds-text-primary) 40%, transparent);
    cursor: not-allowed;
}

.ds-code--disabled .ds-field__help { opacity: var(--ds-opacity-disabled); }

/* Read-only shares the grey ground but keeps the digit legible */
.ds-code--readonly .ds-code__box { background: var(--ds-bg-grayscale-a); }

/* Caret, matching the standard input's hairline */
.ds-code__caret {
    width: 1px;
    background: var(--ds-outline-primary);
}

.ds-code--lg .ds-code__caret { height: 24px; }
.ds-code--sm .ds-code__caret { height: 20px; }

/* The help row is the standard input's, so its size rules are shared */
.ds-code--lg .ds-field__help-icon { width: 20px; height: 20px; }
.ds-code--lg .ds-field__help-icon svg {
    left: 2.5px;
    top: 1.6667px;
    width: 15.0009px;
    height: 13.8339px;
}

.ds-code--sm .ds-field__help-icon { width: 16px; height: 16px; }
.ds-code--sm .ds-field__help-icon svg {
    left: 2px;
    top: 1.3333px;
    width: 12.0007px;
    height: 11.1671px;
}

.ds-code--lg .ds-field__help-text { font-size: 12px; }
.ds-code--sm .ds-field__help-text { font-size: 10px; }

/* ============================================================
   COMPONENT: Dropdown
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame "Dropdown",
   from the exported SVG of the two size columns.

   The box is the Input / Standard field, unchanged: same 44/32
   heights, same padding, same label, help row and caret. Only two
   things differ, and they are the only things styled here —
   the value slot is a <select>, and the trailing slot is a
   chevron that flips when the list is open.
   ============================================================ */

/* ---------- Trigger ----------
   The live control is a button carrying the field box, with the
   droplist hung under it. Static specimens keep the plain div. */

.ds-field__trigger {
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
}

.ds-field__trigger:disabled { cursor: not-allowed; }

/* ---------- Popover ----------
   Anchored to the box, not to the field, so the label above and the
   help row below stay out of the sum. */

.ds-field__anchor { position: relative; display: block; width: 100%; }

.ds-field__menu {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    left: 0;
    z-index: 30;
}

.ds-field__menu[hidden] { display: none; }

/* The frame draws the panel at a fixed 328px; under a trigger it
   tracks the trigger instead. */
.ds-field__menu .ds-droplist { width: 100%; }

.ds-select {
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    height: auto;
    background-image: none;
}

/* Nothing is chosen yet: the resting label is drawn at 40% of
   Text/Primary, the same weight the input gives its placeholder. */
.ds-select.is-resting,
.ds-select:has(option[value=""]:checked) {
    color: color-mix(in srgb, var(--ds-text-primary) 40%, transparent);
}

.ds-select:disabled {
    color: var(--ds-text-primary);
    -webkit-text-fill-color: var(--ds-text-primary);
    cursor: not-allowed;
}

/* The dropped list itself is the browser's, so its options are reset
   back to a normal foreground colour. */
.ds-select option { color: var(--ds-text-primary); }

/* ---------- Chevron ----------
   Unlike the leading icon, this slot is not a 24px box at both
   sizes: Large draws the 24px glyph, Small the 16px one. */

.ds-field__chevron {
    display: block;
    flex: 0 0 auto;
    color: var(--ds-icon-secondary);
    transition: transform .15s ease, color .15s ease;
}

.ds-field__chevron svg { display: block; width: 100%; height: 100%; }

.ds-field--lg .ds-field__chevron { width: 24px; height: 24px; }
.ds-field--sm .ds-field__chevron { width: 16px; height: 16px; }

/* ---------- States ----------
   Only the ones that read differently from Input / Standard. */

/* Open — Outline/Brand, and the chevron turns over */
.ds-field--open .ds-field__control { border-color: var(--ds-outline-brand); }

.ds-field--dropdown .ds-field__control:focus-within .ds-field__chevron,
.ds-field--multi .ds-field__control:focus-within .ds-field__chevron,
.ds-field--focus .ds-field__chevron,
.ds-field--open .ds-field__chevron { color: var(--ds-text-brand); }

.ds-field--open .ds-field__chevron { transform: rotate(180deg); }

/* Open is this component's name for the input's Typing row, so it has
   to take the same brand floated label the shared rule gives Typing. */
.ds-field--open .ds-field__float { color: var(--ds-text-brand); }

/* Disabled and Read-only both dim the chevron; the help row below
   stays at full strength, which is the reverse of the input. Multi-select
   is drawn from the same box and takes the same three chevron rules. */
.ds-field--dropdown.ds-field--disabled .ds-field__chevron,
.ds-field--dropdown.ds-field--readonly .ds-field__chevron,
.ds-field--multi.ds-field--disabled .ds-field__chevron,
.ds-field--multi.ds-field--readonly .ds-field__chevron {
    opacity: var(--ds-opacity-disabled);
}

.ds-field--dropdown.ds-field--disabled .ds-field__help,
.ds-field--multi.ds-field--disabled .ds-field__help { opacity: 1; }

/* ---------- Floating label ----------
   The label treatment is Input / Standard's, unchanged: same resting
   slot, same notch, same 12px / 10px chip. Only one thing differs.

   Read-only is drawn on the plain background here, where the
   adjacent variant takes Bg/Grayscale A — the same split the Text
   area frame carries. Reproduced as drawn. */
.ds-field--dropdown.ds-field--floating.ds-field--readonly .ds-field__control {
    background: var(--ds-bg-background);
}

/* The live trigger is a <button>, so its accessible name comes from
   its own content rather than a <label for>. The float is a span
   inside it, placed first so the name reads "Label Text". */
.ds-field--dropdown .ds-field__float { z-index: 1; }

/* ============================================================
   COMPONENT: Multi-select dropdown
   ------------------------------------------------------------
   Ported from Figma — frames "Multiple selection dropdown /
   Adjacent label" and "... / Floating label" (SVG exports). Two
   size columns, eight states each, both times.

   The box is the Dropdown box unchanged: same heights, padding,
   gaps, chevron, help row and state tokens, so nothing below
   re-states any of it. What the frame adds is a summary chip that
   wraps the leading icon and the selection count while the list is
   open, and the search slot that follows it.
   ============================================================ */

/* ---------- Summary chip ----------
   Closed, this is a bare flex wrapper whose 4px gap is the body's
   own, so [icon][summary] lands exactly where [icon][value] lands in
   the dropdown. Open and holding a selection it paints — and the
   padding it takes is cancelled by an equal negative margin, which is
   why the leading icon does not move by a pixel between the two
   states. Large: 32px tall, 6px in from the outer edge, 6px padding.
   Small: 24px tall, 4px in, 4px padding. */

.ds-field__chip {
    display: flex;
    flex: 0 1 auto;   /* the search slot has no width to give, so a long
                         summary shortens the chip rather than the box */
    align-items: center;
    gap: 4px;
    min-width: 0;
}

.ds-field--open .ds-field__chip.is-filled {
    background: var(--ds-bg-grayscale-a);
}

/* -4px top and bottom: the chip is 8px taller than the 24px content
   row, and must not push the 44px box open. */
.ds-field--lg.ds-field--open .ds-field__chip.is-filled {
    height: 32px;
    margin: -4px 0 -4px -6px;
    padding: 0 6px;
    border-radius: 6px;
}

/* Small needs no vertical correction: its content row is 24px, which
   is the chip's own height. */
.ds-field--sm.ds-field--open .ds-field__chip.is-filled {
    height: 24px;
    margin-left: -4px;
    padding: 0 4px;
    border-radius: 4px;
}

/* ---------- Summary ----------
   The count line — "1 Selected" — on the value's type ramp, since it
   sits in the value's slot. */

.ds-field__summary {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    line-height: 1.5;
    color: var(--ds-text-primary);
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Nothing selected: no empty box, and no stray 4px gap after it. */
.ds-field__summary:empty { display: none; }

.ds-field--lg .ds-field__summary { font-size: 16px; }  /* Body 1 */
.ds-field--sm .ds-field__summary { font-size: 14px; }  /* Body 2 */

/* ---------- Clear ----------
   A 16px glyph centred in a 24px box, so the target clears 24x24 at
   both sizes — on Small the box is the full height of the chip. Drawn
   in Text/Primary, where every other icon in the field is
   Icon/Secondary. */

.ds-field__chip-clear {
    display: none;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    color: var(--ds-text-primary);
    background: transparent;
    border: 0;
    border-radius: 4px;
    cursor: pointer;
}

.ds-field--open .ds-field__chip.is-filled .ds-field__chip-clear { display: flex; }

.ds-field__chip-clear svg { display: block; width: 16px; height: 16px; }

.ds-field__chip-clear:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: -2px;
}

/* ---------- Search slot ----------
   Closed, the resting text is Text/Primary at 40% — the dropdown's
   placeholder. Open, the same words are drawn in Outline/Secondary,
   the colour the Text area gives its resting value. One string, two
   colours; both are reproduced. See the port notes. */

.ds-field--multi.ds-field--open .ds-input::placeholder,
.ds-field--multi.ds-field--open .ds-input--static.is-resting {
    color: var(--ds-outline-secondary);
    opacity: 1;
}

/* The control is a plain box around a real text input rather than a
   button: it has to hold the clear button and the search field, and
   a button cannot contain either. */
.ds-field--multi .ds-field__control { cursor: text; }
.ds-field--multi.ds-field--disabled .ds-field__control { cursor: not-allowed; }
.ds-field--multi .ds-field__chevron { cursor: pointer; }

/* ---------- Floating label ----------
   The notch is Input / Standard's, unchanged: same resting slot, same
   -9px / 10px chip, same 12px / 10px type, so nothing is restated
   here. Three rules is all the floating frame adds.

   1 — Read-only is drawn on the plain background with its count in
   Text/Secondary, where the adjacent frame puts it on Bg/Grayscale A
   in Text/Primary. Reproduced as drawn; see the port notes. */
.ds-field--multi.ds-field--floating.ds-field--readonly .ds-field__control {
    background: var(--ds-bg-background);
}

.ds-field--multi.ds-field--floating.ds-field--readonly .ds-field__summary {
    color: var(--ds-text-secondary);
}

/* 2 — Open keeps the Outline/Secondary search words it takes in the
   adjacent frame. Without this, the input's own floating rule — which
   paints a focused placeholder at 40% of Text/Primary — would win on
   specificity the moment the open field also had focus. */
.ds-field--multi.ds-field--floating.ds-field--open .ds-field__control:focus-within .ds-input::placeholder {
    color: var(--ds-outline-secondary);
    opacity: 1;
}

/* 3 — The label is a real <label for>, so it sits over the input and
   would swallow the click that opens the list; the float's own
   pointer-events: none is what lets that click through, and the
   stacking below keeps the label above the chip's ground while the
   list is open. */
.ds-field--multi .ds-field__float { z-index: 1; }

/* ---------- Search results ----------
   Rows the search has filtered out, and the note that stands in for
   them when every row goes. The droplist frame draws an empty state
   for a panel with no data at all, not for a search that found
   nothing, so this borrows its type and takes the row's padding. */

.ds-field__menu li[hidden] { display: none; }

.ds-droplist__empty-text--filter {
    padding: 8px 12px 12px;
    color: var(--ds-text-secondary);
}

.ds-droplist--sm .ds-droplist__empty-text--filter { font-size: 14px; }

/* ============================================================
   COMPONENT: Droplist
   ------------------------------------------------------------
   Ported from Figma — "Final Design System", frame "Droplist"
   (node 7101:13210), from the exported SVG of its three panels:
   Large, Small, and the empty state.

   The open half of the Dropdown: a floating panel of rows, with
   an optional Select-All header, an optional action footer, and
   an empty state. 328px wide at both sizes; only row height,
   icon box and type scale change between them.
   ============================================================ */

:root {
    /* The frame's drop shadow: y 8, blur 16, #3E134D at 7%. Named for
       the shadow rather than the panel, because the Toggle's knob
       turns out to carry the same one — its filter decodes to these
       exact numbers. */
    --ds-elevation-soft: 0 8px 16px rgba(62, 19, 77, .07);

    /* The item frame carries a second, darker grey under selected and
       pressed rows. Figma's name for it is not in the export; it sits
       one step below Bg/Grayscale A, hence the B. */
    --ds-bg-grayscale-b: #F5F3F7;
}

.ds-droplist {
    display: flex;
    flex-direction: column;
    width: 328px;
    max-width: 100%;
    padding-top: 8px;
    font-family: var(--ds-font);
    position: relative;
    background: var(--ds-bg-background);
    border-radius: var(--ds-corner-radius-8px);
    box-shadow: var(--ds-elevation-soft);
    overflow: hidden;  /* the frame clips its rows to the radius */
}

/* Figma draws the 1px stroke INSIDE the frame and paints it LAST, over
   the rows, which run full-bleed underneath it. A border would add 2px
   to the height and an inset shadow would sit under the rows — so the
   hairline is its own layer on top, and a hovered row can no longer
   swallow the panel's edge. */
.ds-droplist::after {
    position: absolute;
    inset: 0;
    content: "";
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    pointer-events: none;
}

/* The empty state carries its own padding, and its divider sits
   straight under the block — both 8px gaps go. */
.ds-droplist--empty { padding-top: 0; }
.ds-droplist--empty .ds-droplist__footer { margin-top: 0; }

/* ---------- Rows ----------
   The Select-All header and an item are the same row: 12px of side
   padding, then checkbox, leading icon, label and trailing icon at
   8px intervals. */

.ds-droplist__row {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 0;
    padding: 0 12px;
    font-family: inherit;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ds-text-primary);
    text-align: left;
    background: transparent;
    border: 0;
    cursor: pointer;
}

.ds-droplist--lg .ds-droplist__row { height: 44px; font-size: 16px; }  /* Body 1 */
.ds-droplist--sm .ds-droplist__row { height: 32px; font-size: 14px; }  /* Body 2 */

/* ---------- Item states ----------
   Five rows in the item frame: default, hover, selected, disabled and
   one more that shares selected's ground with an empty box. The
   is-* classes force a state for a static specimen; the bare
   selectors drive the live control. */

.ds-droplist__row:hover,
.ds-droplist__row.is-hover { background: var(--ds-bg-grayscale-a); }

/* Selected and pressed share the darker ground. A live row goes
   selected off its own checkbox. */
.ds-droplist__row:active,
.ds-droplist__row.is-pressed,
.ds-droplist__row.is-selected,
.ds-droplist__row:has(.ds-droplist__check:checked) {
    background: var(--ds-bg-grayscale-b);
}

/* Disabled — white ground, and the 40% layer covers the icons and both
   lines of text but NOT the checkbox, which is how the frame draws it. */
.ds-droplist__row:disabled,
.ds-droplist__row.is-disabled,
.ds-droplist__row:has(.ds-droplist__check:disabled) {
    background: var(--ds-bg-background);
    cursor: not-allowed;
}

.ds-droplist__row:disabled .ds-droplist__icon,
.ds-droplist__row:disabled .ds-droplist__text,
.ds-droplist__row.is-disabled .ds-droplist__icon,
.ds-droplist__row.is-disabled .ds-droplist__text,
.ds-droplist__row:has(.ds-droplist__check:disabled) .ds-droplist__icon,
.ds-droplist__row:has(.ds-droplist__check:disabled) .ds-droplist__text {
    opacity: var(--ds-opacity-disabled);
}

.ds-droplist__row:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: -2px;
}

.ds-droplist__header {
    flex: 0 0 auto;
    border-bottom: 1px solid var(--ds-outline-tertiary);
}

/* ---------- Label column ----------
   Title alone, or title over a description. The block is centred in a
   fixed-height row rather than padded, which is what the frame's
   4px (Large) and 1px (Small) leftovers work out to. */

.ds-droplist__text {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
}

.ds-droplist__label,
.ds-droplist__desc {
    display: block;
    overflow: hidden;
    line-height: 1.5;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.ds-droplist__desc { color: var(--ds-text-secondary); }

.ds-droplist--lg .ds-droplist__desc { font-size: 12px; }  /* Body 3 */
.ds-droplist--sm .ds-droplist__desc { font-size: 10px; }  /* Description */

/* A description makes the row taller: 44 -> 50 and 32 -> 38 */
.ds-droplist--lg .ds-droplist__row.has-description { height: 50px; }
.ds-droplist--sm .ds-droplist__row.has-description { height: 38px; }

/* ---------- Scrolling list ----------
   The frame shows ten rows before the thumb appears, at both sizes. */

.ds-droplist__list {
    flex: 1 1 auto;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--ds-outline-tertiary) transparent;
}

.ds-droplist--lg .ds-droplist__list { max-height: 440px; }  /* 10 x 44 */
.ds-droplist--sm .ds-droplist__list { max-height: 320px; }  /* 10 x 32 */

.ds-droplist__list::-webkit-scrollbar { width: 4px; }
.ds-droplist__list::-webkit-scrollbar-track { background: transparent; }

.ds-droplist__list::-webkit-scrollbar-thumb {
    background: var(--ds-outline-tertiary);
    border-radius: 1px;
}

/* ---------- Icons ----------
   One 24-box glyph at every size; the box shrinks and the stroke is
   held at 1px with vector-effect, which is how the frame draws it. */

.ds-droplist__icon {
    display: block;
    flex: 0 0 auto;
    color: var(--ds-icon-secondary);
}

.ds-droplist__icon svg { display: block; width: 100%; height: 100%; }

/* A value on the same line as the label, pushed to the end of the
   row. The text block beside it already flexes, so this only has to
   stop shrinking. */
.ds-droplist__meta {
    flex: 0 0 auto;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    color: var(--ds-text-secondary);
    white-space: nowrap;
}

/* A leading mark that is a colour rather than a glyph. It fills the
   frame's icon box, so the row's geometry is unchanged. The hairline
   is so the near-white values still read as tiles. */
.ds-droplist__swatch {
    border-radius: var(--ds-corner-radius-4px);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12);
}

.ds-droplist--lg .ds-droplist__icon { width: 24px; height: 24px; }
.ds-droplist--sm .ds-droplist__icon { width: 20px; height: 20px; }

/* ---------- Checkbox ----------
   18px at both sizes. The frame draws no checked state — see the
   port notes; the tick below is this build's, not the library's. */

.ds-droplist__check {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The same control the Checkbox component draws, so it takes that
   frame's border and glyphs rather than a copy of its own. The
   Outline/Tertiary this used to carry was the panel's border colour,
   not a measurement of the box. */
.ds-droplist__box {
    position: relative;
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-secondary);
    border-radius: 4px;
}

/* Checked keeps the white box and the grey outline and drops a
   Text/Brand tick inside it — the exported glyph, in the box's own
   18px coordinates. */
.ds-droplist__box::after {
    position: absolute;
    inset: 0;
    content: "";
    background: no-repeat center / 18px 18px var(--ds-check-tick);
    opacity: 0;
}

.ds-droplist__check:checked + .ds-droplist__box::after { opacity: 1; }

/* Some rows on, some off — the state the multi-select dropdown's
   Select-All row lands in as soon as one option is picked. The
   Droplist frame draws no mixed box, so this build invented one at
   9x1.5 with rounded ends; the Checkbox frame has since drawn the
   real thing at 10x2 square, and this follows it. */
.ds-droplist__check:indeterminate + .ds-droplist__box::after {
    opacity: 1;
    background: no-repeat center / 18px 18px var(--ds-check-dash);
}

.ds-droplist__check:focus-visible + .ds-droplist__box {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 1px;
}

/* ---------- Footer action ----------
   44px tall at both sizes, centred, Text/Brand throughout. */

.ds-droplist__footer {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: 44px;
    margin-top: 8px;
    padding: 0 12px;
    font-family: inherit;
    font-size: 14px;  /* Body 2 */
    font-weight: 400;
    line-height: 1.5;
    color: var(--ds-text-brand);
    background: transparent;
    border: 0;
    border-top: 1px solid var(--ds-outline-tertiary);
    cursor: pointer;
}

.ds-droplist__footer .ds-droplist__icon { width: 20px; height: 20px; color: currentColor; }

.ds-droplist__footer:hover { background: var(--ds-bg-grayscale-a); }

.ds-droplist__footer:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: -2px;
}

/* ---------- Empty state ---------- */

.ds-droplist__empty {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 20px 12px 12px;
    text-align: center;
}

.ds-droplist__empty .ds-droplist__icon { width: 24px; height: 24px; }

.ds-droplist__empty-text {
    margin: 0;  /* the block is a <p>; its UA margin would break the 12px gap */
    font-size: 16px;  /* Body 1 */
    line-height: 1.5;
    color: var(--ds-text-primary);
}

/* Galleries of wide specimens (the droplist panel is a fixed 328px) */
.dsx-specimens--wide { grid-template-columns: repeat(auto-fill, minmax(min(376px, 100%), 1fr)); }

/* ============================================================
   COMPONENT: Button
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Buttons" frame: three
   tone columns x five styles x two sizes x four state rows.

   The grounds below carry invented names — an SVG export has no
   variable names in it — but the neutral tone needs none of them:
   it is Text/Primary, Text/Secondary and the two greyscale grounds
   the rest of the library already uses.
   ============================================================ */

:root {
    --ds-bg-brand: #2797E8;
    --ds-bg-brand-hover: #52ACED;
    --ds-bg-brand-soft: #E7F4FE;
    --ds-bg-brand-soft-hover: #D4EAFA;

    --ds-bg-error: #FF525F;
    --ds-bg-error-hover: #FF757F;
    --ds-bg-error-soft: #FFEAEC;
    --ds-bg-error-soft-hover: #FFDCDF;
}

.ds-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    border: 0;
    border-radius: var(--ds-corner-radius-8px);
    background: transparent;
    font-family: var(--ds-font);
    /* The Typography frame names Button as Medium. This was built at
       SemiBold from measuring the drawn button, before that frame was
       ported — a named value beats a measured one, and this is what
       --ds-button-1..3 have said all along. */
    font-weight: var(--ds-font-weight-medium);
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
}

/* ---------- Sizes ---------- */

.ds-btn--lg { height: 44px; padding: 0 20px; font-size: 16px; }  /* Body 1 */
.ds-btn--sm { height: 32px; padding: 0 12px; font-size: 14px; }  /* Body 2 */

.ds-btn__icon { display: inline-flex; flex: 0 0 auto; }
.ds-btn__icon svg { display: block; }
.ds-btn--lg .ds-btn__icon svg { width: 24px; height: 24px; }
.ds-btn--sm .ds-btn__icon svg { width: 20px; height: 20px; }

/* ---------- Tones ----------
   One ground and three derivatives; every style below reads them. */

.ds-btn--brand {
    --btn-ground: var(--ds-bg-brand);
    --btn-ground-hover: var(--ds-bg-brand-hover);
    --btn-soft: var(--ds-bg-brand-soft);
    --btn-soft-hover: var(--ds-bg-brand-soft-hover);
}

.ds-btn--danger {
    --btn-ground: var(--ds-bg-error);
    --btn-ground-hover: var(--ds-bg-error-hover);
    --btn-soft: var(--ds-bg-error-soft);
    --btn-soft-hover: var(--ds-bg-error-soft-hover);
}

.ds-btn--neutral {
    --btn-ground: var(--ds-text-primary);
    --btn-ground-hover: var(--ds-text-secondary);
    --btn-soft: var(--ds-bg-grayscale-a);
    --btn-soft-hover: var(--ds-bg-grayscale-b);
}

/* ---------- Styles ---------- */

.ds-btn--filled {
    background: var(--btn-ground);
    /* The label on a filled button is Bg/Grayscale A, not white */
    color: var(--ds-bg-grayscale-a);
}

.ds-btn--filled:not(:disabled):hover,
.ds-btn--filled.is-hover { background: var(--btn-ground-hover); }

.ds-btn--tonal {
    background: var(--btn-soft);
    color: var(--btn-ground);
}

.ds-btn--tonal:not(:disabled):hover,
.ds-btn--tonal.is-hover { background: var(--btn-soft-hover); }

/* Figma strokes the box inward, so the ring must not add to the
   size the way a border would. */
.ds-btn--outline {
    background: var(--ds-bg-background);
    color: var(--btn-ground);
    box-shadow: inset 0 0 0 1px var(--btn-ground);
}

.ds-btn--outline:not(:disabled):hover,
.ds-btn--outline.is-hover { background: var(--btn-soft); }

.ds-btn--ghost {
    background: transparent;
    color: var(--btn-ground);
}

/* A step darker than the outline's hover — the frame is consistent
   about this across all three tones. */
.ds-btn--ghost:not(:disabled):hover,
.ds-btn--ghost.is-hover { background: var(--btn-soft-hover); }

/* ---------- Link ----------
   No padding and no ground: the box is the content, and the rule
   rides its bottom edge. */

.ds-btn--link {
    height: auto;
    padding: 0;
    background: none;
    border-radius: 0;
    color: var(--btn-ground);
}

.ds-btn--lg.ds-btn--link { min-height: 24px; }

/* The one place the 8px gap does not hold: every Small link column
   in the frame draws 4px, which is what puts the box at 125px
   against the Small button's 133px of content. */
.ds-btn--sm.ds-btn--link { min-height: 20px; gap: 4px; }

.ds-btn--link:not(:disabled):hover,
.ds-btn--link.is-hover { opacity: .8; }

/* The rule under a link, in every state and disabled too. A gradient
   rather than a dashed border, so the 2-on / 2-off the export draws
   is exact.

   It used to be a separate --dashed modifier, with the plain link
   taking a solid underline on hover and nothing at rest. A link you
   cannot see until you point at it is not a link, and having two of
   them meant choosing. There is one now, and it is always drawn. */
.ds-btn--link {
    background-image: linear-gradient(to right, currentColor 0 2px, transparent 2px 4px);
    background-size: 4px 1px;
    background-repeat: repeat-x;
    background-position: left bottom;
}

/* ---------- States ----------
   Pressed needs no rule outside the link: the frame draws the third
   row exactly like the first. Disabled is Figma's 40% layer. */

.ds-btn:disabled {
    opacity: var(--ds-opacity-disabled);
    cursor: not-allowed;
}

/* Nothing in the frame draws focus; this ring is this build's. */
.ds-btn:focus-visible {
    outline: 2px solid var(--btn-ground);
    outline-offset: 2px;
}

/* ============================================================
   COMPONENT: Icon button
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Icon buttons" frame.
   The grounds are the Button's to the hex, so this is a modifier
   on .ds-btn: only the square box, the third size and the glyph
   weights live here.
   ============================================================ */

.ds-iconbtn {
    padding: 0;
    gap: 0;
    flex: 0 0 auto;
}

.ds-iconbtn--lg { width: 44px; height: 44px; border-radius: var(--ds-corner-radius-8px); }
.ds-iconbtn--md { width: 32px; height: 32px; border-radius: var(--ds-corner-radius-8px); }
/* The smallest box halves its radius — consistent across all
   twelve of the 24px cells in the frame. */
.ds-iconbtn--sm { width: 24px; height: 24px; border-radius: 4px; }

.ds-iconbtn--lg .ds-btn__icon svg { width: 24px; height: 24px; }
.ds-iconbtn--md .ds-btn__icon svg { width: 20px; height: 20px; }
.ds-iconbtn--sm .ds-btn__icon svg { width: 16px; height: 16px; }

/* The export strokes the 24px glyph at 1.5 and the other two at 1.
   vector-effect on the paths keeps that in screen pixels rather
   than a scaled fraction of the 24 viewBox. */
.ds-iconbtn--lg .ds-btn__icon svg { stroke-width: 1.5; }
.ds-iconbtn--md .ds-btn__icon svg,
.ds-iconbtn--sm .ds-btn__icon svg { stroke-width: 1; }

/* ============================================================
   COMPONENT: Text area
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame "Text area"
   (SVG export). One size x two label treatments (adjacent /
   floating) x eight states.

   Every variant in the frame is 318 x 50 with the text 8px from
   the top and 12px from the left, so the box is top-aligned, not
   centred. Every variant also carries a scrollbar thumb and a
   resize grip, which is why both ship as part of the control
   rather than as overflow-driven chrome.
   ============================================================ */

:root {
    /* Outline/Secondary — the export's resting-value and grip
       colour. Named by family: the SVG carries the hex only. */
    --ds-outline-secondary: #D9D1E0;
}

.ds-textarea {
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
    font-family: var(--ds-font);
}

/* ---------- Adjacent label ---------- */

.ds-textarea__label {
    display: flex;
    align-items: center;
    margin: 0;
    font-size: 14px;  /* Body 2 */
    line-height: 1.5;
    color: var(--ds-text-secondary);
}

/* ---------- Control ----------
   Figma draws the 1px stroke inside the frame, so it does not add
   to the height: the box measures 50px overall with the content
   8px / 12px from the outer edge. The padding below is that value
   minus the border, which lands the content exactly there. */

.ds-textarea__control {
    position: relative;
    box-sizing: border-box;
    display: flex;
    width: 100%;
    padding: 7px 11px;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

/* ---------- Field ---------- */

.ds-textarea__field {
    display: block;
    flex: 1 0 0;
    width: 100%;
    min-width: 0;
    /* 50 − 2 border − 14 padding = the 34px content box the frame
       draws. Taller rows grow from there in 24px line steps. */
    min-height: 34px;
    margin: 0;
    padding: 0;
    font-family: inherit;
    font-size: 16px;    /* Body 1 */
    font-weight: 400;
    line-height: 24px;  /* 1.5 */
    color: var(--ds-text-primary);
    background: transparent;
    border: 0;
    border-radius: 0;
    outline: none;
    resize: vertical;
    overflow: auto;
    appearance: none;
    /* Overlay scrollbar, thumb pinned to the drawn colour.
       ::-webkit-scrollbar would buy an exact 4px thumb at exactly
       4-8px in, but only as a classic bar that takes 10px of layout
       the moment content overflows — which reflows every line of the
       user's text sideways as they type past the first box. Chrome
       121+ ignores the -webkit- pseudo-elements once these two
       standard properties are set, so this is the whole rule: the
       overlay bar is ~2px wider than drawn and never moves the text. */
    scrollbar-width: thin;
    scrollbar-color: var(--ds-outline-tertiary) transparent;
}

/* The field runs out into the control's right padding so its overlay
   bar lands in the gutter the frame draws it in, then pays the 11px
   back as its own padding — text still stops 12px from the outer edge,
   with or without overflow. */
.ds-textarea__field { margin-right: -11px; padding-right: 11px; }

/* The frame paints the resting value a flat Outline/Secondary
   rather than the 40% layer Input / Standard uses. */
.ds-textarea__field::placeholder {
    color: var(--ds-outline-secondary);
    opacity: 1;
}

/* ---------- Scrollbar, drawn ----------
   The frame draws a 4px thumb, 24px tall, inset 4px from the top and
   right of the outer edge, on all sixteen variants — including the
   empty ones, where a real scrollbar would not appear. It is rendered
   in the specimen galleries so they match the frame; the live control
   uses the styled native bar above, which actually scrolls. */

.ds-textarea__scrollbar {
    position: absolute;
    top: 7px;
    right: 3px;
    width: 4px;
    height: 24px;
    background: var(--ds-outline-tertiary);
    border-radius: 1px;
    pointer-events: none;
}

/* ---------- Resize grip, drawn ----------
   Three hairlines 3px apart in a 9px box, inset 4px from the
   bottom-right of the outer edge. Specimen-only, for the same reason
   as the thumb: the live control carries the browser's own resizer,
   which is a real drag target. */

.ds-textarea__grip {
    position: absolute;
    right: 3px;
    bottom: 3px;
    width: 9px;
    height: 9px;
    color: var(--ds-outline-secondary);
    pointer-events: none;
}

.ds-textarea__grip svg { display: block; width: 9px; height: 9px; }

/* ---------- Static value slot ----------
   Specimen galleries render the value as text rather than a live
   textarea, so a gallery does not fill the page with tab stops. */

.ds-textarea__value {
    display: block;
    flex: 1 0 0;
    min-width: 0;
    min-height: 34px;
    font-size: 16px;
    line-height: 24px;
    color: var(--ds-text-primary);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.ds-textarea__value.is-resting { color: var(--ds-outline-secondary); }

/* ---------- Help row ----------
   The markup is Input / Standard's, whose size rules are scoped to
   .ds-field--lg / --sm; the text area has one size, so it pins the
   Large geometry here. */

.ds-textarea .ds-field__help-icon { width: 20px; height: 20px; }

.ds-textarea .ds-field__help-icon svg {
    left: 2.5px;
    top: 1.6667px;
    width: 15.0009px;
    height: 13.8339px;
}

.ds-textarea .ds-field__help-text { font-size: 12px; }  /* Body 3 */

/* ============================================================
   Floating label
   ============================================================ */

/* Resting: the label sits on the first text line — top-aligned,
   not centred the way the single-line input rests it. */
.ds-textarea__float {
    position: absolute;
    left: 11px;
    top: 7px;
    display: flex;
    align-items: center;
    height: 24px;
    font-size: 16px;
    line-height: 1.5;
    color: var(--ds-text-secondary);
    white-space: nowrap;
    pointer-events: none;
    transition: top .15s ease, height .15s ease, font-size .15s ease, color .15s ease;
}

/* Floated: notched into the top border on a background-coloured
   chip. The chip's left edge lands on the resting text origin, so
   the label steps 2px right as it lifts — as drawn. */
.ds-textarea.is-floated .ds-textarea__float,
.ds-textarea--floating .ds-textarea__control:focus-within .ds-textarea__float,
.ds-textarea--floating .ds-textarea__control:has(.ds-textarea__field:not(:placeholder-shown)) .ds-textarea__float {
    top: -10px;
    height: 18px;
    padding: 0 2px;
    font-size: 12px;
    background: var(--ds-bg-background);
}

/* While the label still rests on the first line it would collide
   with the placeholder, so the hint only appears once it lifts. */
.ds-textarea--floating .ds-textarea__field::placeholder { color: transparent; }

.ds-textarea--floating .ds-textarea__control:focus-within .ds-textarea__field::placeholder,
.ds-textarea--floating.is-floated .ds-textarea__field::placeholder {
    color: var(--ds-outline-secondary);
}

/* ============================================================
   States
   ============================================================ */

/* Hover — the frame draws it identically to Default
   (Outline/Tertiary). Kept as its own rule so adding a hover
   outline later is a one-line change. */
.ds-textarea--hover .ds-textarea__control { border-color: var(--ds-outline-tertiary); }

/* Focus + Typing — Outline/Brand, and the floated label goes brand */
.ds-textarea__control:focus-within,
.ds-textarea--focus .ds-textarea__control,
.ds-textarea--typing .ds-textarea__control {
    border-color: var(--ds-outline-brand);
}

.ds-textarea--focus .ds-textarea__float,
.ds-textarea--typing .ds-textarea__float,
.ds-textarea--floating .ds-textarea__control:focus-within .ds-textarea__float {
    color: var(--ds-text-brand);
}

/* Entered — back to Outline/Tertiary, value at full strength */
.ds-textarea--entered .ds-textarea__control { border-color: var(--ds-outline-tertiary); }

/* Error — Outline/Error and a red help row. The floated label
   stays Text/Secondary here, where Input / Standard turns it red. */
.ds-textarea--error .ds-textarea__control,
.ds-textarea--error .ds-textarea__control:focus-within {
    border-color: var(--ds-outline-error);
}

.ds-textarea--error .ds-field__help-icon { color: var(--ds-icon-error); }
.ds-textarea--error .ds-field__help-text { color: var(--ds-text-error); }

/* Disabled — Bg/Grayscale A behind the field. The adjacent variant
   drops its resting value to a flat Outline/Tertiary; the floating
   variant keeps its resting label and dims it to 40%. Neither the
   adjacent label nor the help row is dimmed, as drawn. */
.ds-textarea--disabled .ds-textarea__control {
    background: var(--ds-bg-grayscale-a);
    border-color: var(--ds-outline-tertiary);
}

.ds-textarea--disabled .ds-textarea__field::placeholder,
.ds-textarea--disabled .ds-textarea__value.is-resting {
    color: var(--ds-outline-tertiary);
}

.ds-textarea--floating.ds-textarea--disabled .ds-textarea__float {
    opacity: var(--ds-opacity-disabled);
}

.ds-textarea__field:disabled {
    color: var(--ds-text-primary);
    -webkit-text-fill-color: var(--ds-text-primary);
    resize: none;
    cursor: not-allowed;
}

/* Read-only — the adjacent variant takes the grey ground and keeps
   its value at Text/Primary; the floating variant is drawn on the
   plain background with a Text/Secondary value. Both as designed. */
.ds-textarea--readonly .ds-textarea__control {
    background: var(--ds-bg-grayscale-a);
    border-color: var(--ds-outline-tertiary);
}

.ds-textarea--readonly .ds-textarea__control:focus-within {
    border-color: var(--ds-outline-tertiary);
}

.ds-textarea--floating.ds-textarea--readonly .ds-textarea__control {
    background: var(--ds-bg-background);
}

.ds-textarea--floating.ds-textarea--readonly .ds-textarea__field,
.ds-textarea--floating.ds-textarea--readonly .ds-textarea__value {
    color: var(--ds-text-secondary);
}

/* ============================================================
   Text area — proposed corrections
   ------------------------------------------------------------
   Off by default: everything above is what Figma draws. Each rule
   here resolves one row of the Design review table, and each one
   reuses a token this frame already carries rather than inventing
   a new one — so adopting a fix is a Figma edit, not a new colour.
   ============================================================ */

/* 1 — Resting value. The same frame paints the floating variant's
   resting label Text/Secondary; the adjacent variant paints the
   same role Outline/Secondary at 1.48:1. One role, one colour. */
.ds-textarea--proposed .ds-textarea__field::placeholder,
.ds-textarea--proposed .ds-textarea__value.is-resting {
    color: var(--ds-text-secondary);
}

/* 2 — Disabled value. Dim Text/Secondary by the same 40% the
   floating variant already applies to its own label, instead of a
   flat Outline/Tertiary that is the colour of the border it sits
   inside. */
.ds-textarea--proposed.ds-textarea--disabled .ds-textarea__field::placeholder,
.ds-textarea--proposed.ds-textarea--disabled .ds-textarea__value.is-resting {
    color: color-mix(in srgb, var(--ds-text-secondary) 40%, transparent);
}

/* 3 — Hover. Outline/Secondary is one step darker than the resting
   border and already lives in this frame, so the pointer gets an
   answer without a new token. Focus and Error still outrank it. */
.ds-textarea--proposed:not(.ds-textarea--error):not(.ds-textarea--disabled):not(.ds-textarea--readonly)
    .ds-textarea__control:hover:not(:focus-within),
.ds-textarea--proposed.ds-textarea--hover .ds-textarea__control {
    border-color: var(--ds-outline-secondary);
}

/* 4 — Focus. A 3px halo derived from the existing outline colour, so
   the state survives a busy page instead of resting on one pixel. */
.ds-textarea--proposed.ds-textarea--focus .ds-textarea__control,
.ds-textarea--proposed.ds-textarea--typing .ds-textarea__control,
.ds-textarea--proposed .ds-textarea__control:focus-within {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ds-outline-brand) 20%, transparent);
}

/* An error field only haloes while it has focus — a permanent red glow
   on every invalid field turns the page into an alarm. Higher
   specificity than the brand rule above, so error keeps its own hue
   when a field is both invalid and focused. */
.ds-textarea--proposed.ds-textarea--error .ds-textarea__control:focus-within,
.ds-textarea--proposed.ds-textarea--error.ds-textarea--focus .ds-textarea__control,
.ds-textarea--proposed.ds-textarea--error.ds-textarea--typing .ds-textarea__control {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ds-outline-error) 20%, transparent);
}

/* 5 — Error. The floated label goes red, the way Input / Standard
   already draws it. */
.ds-textarea--proposed.ds-textarea--error.is-floated .ds-textarea__float,
.ds-textarea--proposed.ds-textarea--error .ds-textarea__control:focus-within .ds-textarea__float,
.ds-textarea--proposed.ds-textarea--error .ds-textarea__control:has(.ds-textarea__field:not(:placeholder-shown)) .ds-textarea__float {
    color: var(--ds-text-error);
}

/* 6 — Read-only. The floating variant takes the adjacent variant's
   ground and value, so one state does not read as two components. */
.ds-textarea--proposed.ds-textarea--floating.ds-textarea--readonly .ds-textarea__control {
    background: var(--ds-bg-grayscale-a);
}

.ds-textarea--proposed.ds-textarea--floating.ds-textarea--readonly .ds-textarea__field,
.ds-textarea--proposed.ds-textarea--floating.ds-textarea--readonly .ds-textarea__value {
    color: var(--ds-text-primary);
}

/* ============================================================
   COMPONENT: Search
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Search" frame
   (735x671): one type x two sizes (Large 300x44 / Small 300x32)
   x five states — Default, Hover, Focus, Typing, Entered.
   No label, and no Error / Disabled / Read-only column.

   Measured off the export: the leading glyph sits in a 20px box
   (16 Small) 10px (8) in from the outer edge, the value 10px (8)
   after it, the caret 4px after the value, and the clear glyph
   10px (8) in from the right edge. The help row lands on exactly
   the coordinates Input / Standard already draws — icon box
   flush with the field's left edge, 4px under it — so that block
   is reused here rather than copied.
   ============================================================ */

:root {
    /* Icon/Primary — this frame paints the search and clear glyphs
       Text/Primary, where every other field uses Icon/Secondary. */
    --ds-icon-primary: #3D4C66;
}

.ds-search {
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
    font-family: var(--ds-font);
}

/* ---------- Control ---------- */

.ds-search__control {
    position: relative;
    display: flex;
    box-sizing: border-box;
    align-items: center;
    width: 100%;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

/* Figma draws the 1px stroke INSIDE the frame, so it does not add to
   the height: Large measures 44px overall and Small 32px. The padding
   is the Figma value minus that pixel, which puts the glyph boxes at
   exactly 10px (Large) and 8px (Small) from the outer edge. */
.ds-search--lg .ds-search__control { height: 44px; gap: 10px; padding: 0 9px; }
.ds-search--sm .ds-search__control { height: 32px; gap: 8px; padding: 0 7px; }

/* Value + caret are one group 4px apart, the way every field in the
   library draws its insertion point; the 10px/8px control gap is what
   separates that group from the two glyphs. */
.ds-search__body {
    display: flex;
    flex: 1 0 0;
    align-items: center;
    gap: 4px;
    min-width: 0;
}

/* ---------- Glyphs: Search leading, Close trailing ---------- */

.ds-search__icon {
    flex: 0 0 auto;
    display: block;
    color: var(--ds-icon-primary);
}

.ds-search__clear {
    flex: 0 0 auto;
    display: block;
    box-sizing: content-box;  /* so the proposed hit area can grow without moving the glyph */
    padding: 0;
    color: var(--ds-icon-primary);
    background: none;
    border: 0;
    cursor: pointer;
}

.ds-search__icon svg,
.ds-search__clear svg { display: block; }

.ds-search--lg .ds-search__icon,
.ds-search--lg .ds-search__icon svg,
.ds-search--lg .ds-search__clear,
.ds-search--lg .ds-search__clear svg { width: 20px; height: 20px; }

.ds-search--sm .ds-search__icon,
.ds-search--sm .ds-search__icon svg,
.ds-search--sm .ds-search__clear,
.ds-search--sm .ds-search__clear svg { width: 16px; height: 16px; }

/* ---------- Value ---------- */

.ds-search--lg .ds-input { font-size: 16px; }  /* Body 1 */
.ds-search--sm .ds-input { font-size: 14px; }  /* Body 2 */

.ds-search--lg .ds-input--static { min-height: 24px; }
.ds-search--sm .ds-input--static { min-height: 21px; }

.ds-search--lg .ds-field__caret { height: 24px; }
.ds-search--sm .ds-field__caret { height: 20px; }

/* type="search" brings WebKit's own cancel button, which would sit
   next to the one the frame draws. */
.ds-search__input::-webkit-search-cancel-button,
.ds-search__input::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}

/* While the field is empty the leading glyph rests at 40% alongside the
   placeholder — the frame puts a 40% layer over both. `is-resting` is
   the specimen-gallery equivalent, where the value is a span. */
.ds-search__control:has(.ds-search__input:placeholder-shown) .ds-search__icon,
.ds-search.is-resting .ds-search__icon { opacity: var(--ds-opacity-disabled); }

/* The clear glyph is drawn only once the field holds a value */
.ds-search__control:has(.ds-search__input:placeholder-shown) .ds-search__clear { display: none; }

/* ============================================================
   Search — states
   ============================================================ */

/* Hover — the frame draws it identically to Default (Outline/Tertiary).
   Kept as its own rule so adopting the redline is a one-line change. */
.ds-search--hover .ds-search__control { border-color: var(--ds-outline-tertiary); }

/* Entered — back to Outline/Tertiary with the value at full strength */
.ds-search--entered .ds-search__control { border-color: var(--ds-outline-tertiary); }

/* Focus + Typing — Outline/Brand. Last of the three so that a live
   field carrying a value still turns blue when it takes focus. */
.ds-search--focus .ds-search__control,
.ds-search--typing .ds-search__control,
.ds-search__control:focus-within {
    border-color: var(--ds-outline-brand);
}

/* ============================================================
   Search — proposed corrections
   ------------------------------------------------------------
   Off by default: everything above is what Figma draws. Each rule
   here is numbered against the Design review table in the
   component page, and switched on together by the Fidelity
   control, which adds .ds-search--proposed.
   ============================================================ */

/* 1 — Resting value and glyph. Text/Primary at 40% blends to 2.01:1
   on white; Text/Secondary at full strength is 3.68:1 and is the
   colour this library already uses for resting text. */
.ds-search--proposed .ds-input::placeholder {
    color: var(--ds-text-secondary);
    opacity: 1;
}

.ds-search--proposed .ds-input--static.is-resting {
    color: var(--ds-text-secondary);
    opacity: 1;
}

.ds-search--proposed .ds-search__control:has(.ds-search__input:placeholder-shown) .ds-search__icon,
.ds-search--proposed.is-resting .ds-search__icon {
    color: var(--ds-icon-secondary);
    opacity: 1;
}

/* 2 — Hover answers the pointer, one step darker on the outline.
   Outline/Secondary is already in the library, from Text area. */
.ds-search--proposed.ds-search--hover .ds-search__control,
.ds-search--proposed .ds-search__control:hover {
    border-color: var(--ds-outline-secondary);
}

/* 3 — Focus carries a halo as well as the 1px border: 20% of
   Outline/Brand, not a new hue. Placed after the hover rule so a
   hovered, focused field still reads as focused. */
.ds-search--proposed.ds-search--focus .ds-search__control,
.ds-search--proposed.ds-search--typing .ds-search__control,
.ds-search--proposed .ds-search__control:focus-within {
    border-color: var(--ds-outline-brand);
    box-shadow: 0 0 0 3px rgba(39, 151, 232, .2);
}

/* 4 — The caret. Figma draws Focus with no insertion point at all;
   the element is rendered by the component for this fidelity only,
   and the negative margin sits it ON the text origin rather than
   in front of it. */

/* 5 — The clear button is a 20px (16px) target, under the 24px
   minimum. Padding grows the hit area and the negative margin gives
   the layout back, so not a pixel of the drawing moves. */
.ds-search--proposed.ds-search--lg .ds-search__clear { padding: 2px; margin: -2px; }
.ds-search--proposed.ds-search--sm .ds-search__clear { padding: 4px; margin: -4px; }

/* ============================================================
   COMPONENT: Toast
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Alert" frame (567x942):
   five Large cards — Info, Success, Warning, Error, and Info on a
   dark ground — over four Small ones.

   Anatomy is the same at both sizes: a 5px accent rule down the
   left edge, a 16px filled glyph, the message, and a close X.
   The accent is the only thing the tone changes; the ground stays
   white so a page of alerts does not turn into a colour chart.
   ============================================================ */

:root {
    /* Accents. Brand and Error are the library's own; the other two
       are new with this frame and carry invented names — an SVG
       export has no variable names in it. */
    --ds-bg-success: #47D2A5;
    --ds-bg-warning: #FF8552;

    /* The dark ground and the text that sits on it. Both hexes are
       already in the library under other roles: the ground is
       Text/Primary and the text is Bg/Grayscale A. */
    --ds-bg-inverse: #3D4C66;
    --ds-text-inverse: #FAF9FC;

    /* The frame's drop shadow: y 15, blur 30, #3E134D at 9% — twice
       the lift the droplist carries. */
    --ds-elevation-alert: 0 15px 30px rgba(62, 19, 77, .09);
}

.ds-toast {
    --alert-accent: var(--ds-bg-brand);

    display: flex;
    align-items: flex-start;
    width: 100%;
    border: 1px solid var(--ds-outline-tertiary);
    border-left: 5px solid var(--alert-accent);
    border-radius: var(--ds-corner-radius-8px);
    background: var(--ds-bg-background);
    box-shadow: var(--ds-elevation-alert);
    font-family: var(--ds-font);
    color: var(--ds-text-primary);
}

/* ---------- Tones ----------
   One line each: the rule and the glyph read the same value. */

.ds-toast--info    { --alert-accent: var(--ds-bg-brand); }
.ds-toast--success { --alert-accent: var(--ds-bg-success); }
.ds-toast--warning { --alert-accent: var(--ds-bg-warning); }
.ds-toast--error   { --alert-accent: var(--ds-bg-error); }

/* ---------- Sizes ----------
   The paddings are stated net of the border. Figma draws the 1px
   outline straddling the card bounds and the 5px rule inside them,
   so the insets that matter — 13px above the text, 16px in from
   each edge — come out as 12/15 here plus the border, and 16 on
   the left where the rule has already eaten 5. Four lines of Body 2
   then land the Large card at exactly 110px.

   Small holds a 40px minimum, which centres its single line in the
   card, and hugs its message — the frame draws the four of them at
   301, 210, 210 and 251px. */

.ds-toast--lg { gap: 16px; padding: 12px 15px 12px 16px; }

.ds-toast--sm {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    padding: 8px 11px 8px 12px;
    min-height: 40px;
    width: auto;
}

/* ---------- Glyph ----------
   The 21px box is the message's line box, so the glyph centres on
   the first line however many lines follow it. */

.ds-toast__icon {
    display: flex;
    align-items: center;
    flex: 0 0 16px;
    height: 21px;
    color: var(--alert-accent);
}

.ds-toast__glyph { display: block; width: 16px; height: 16px; }
.ds-toast__glyph-shape { fill: currentColor; }

.ds-toast__glyph-mark {
    fill: none;
    stroke: var(--ds-bg-grayscale-a);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ---------- Body ----------
   Everything between the glyph and the close: an optional head row
   (title, and a link pushed to the far end), the message, and an
   optional row of actions. The gaps are the frame's own — 8px under
   the head, 16px over the actions — and they are stated as margins
   on the parts that need them rather than one gap on the column, so
   a message on its own carries none of them. */

.ds-toast__body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
}

.ds-toast__head { display: flex; align-items: baseline; gap: 16px; }

.ds-toast__title {
    margin: 0;
    font-size: 14px;      /* Body 2 */
    font-weight: 600;     /* SemiBold — measured. The scale has no bold row this small */
    line-height: 21px;
}

/* Pushed to the end of the head row, and still there when the title
   is absent. */
.ds-toast__link {
    flex: 0 0 auto;
    margin-left: auto;
    font-size: 14px;
    font-weight: 400;
    line-height: 21px;
    color: var(--ds-text-brand);
    text-decoration: none;
    cursor: pointer;
}

/* Not drawn: a link that answers only in colour fails anyone who
   cannot see the hue. */
.ds-toast__link:hover { text-decoration: underline; }
.ds-toast__link:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
    border-radius: 2px;
}

.ds-toast__message {
    margin: 0;
    font-size: 14px;      /* Body 2 */
    font-weight: 400;
    line-height: 21px;
}

.ds-toast__head + .ds-toast__message { margin-top: 8px; }

/* ---------- Actions ----------
   Button/Small, straight from the Button component. The frame draws
   two, filled then tonal, 8px apart. */

.ds-toast__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;
}

/* ---------- Close ----------
   16px of glyph in a 24px target: the padding grows the hit area
   and the negative margin gives the layout back, so nothing in the
   drawing moves. */

.ds-toast__close {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    height: 21px;
    padding: 4px;
    margin: -4px;
    border: 0;
    background: none;
    color: var(--ds-text-primary);
    border-radius: 4px;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
}

.ds-toast__close svg { display: block; width: 16px; height: 16px; }

/* Not drawn in the frame — a bare X with no answer to the pointer
   reads as decoration. */
.ds-toast__close:hover { background: var(--ds-bg-grayscale-a); }
.ds-toast__close:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 1px;
}

/* ---------- Dark ground ----------
   Drawn once, on the Info card. The border goes entirely: on
   #3D4C66 the 1px Outline/Tertiary would be a light seam. */

.ds-toast--dark {
    background: var(--ds-bg-inverse);
    border-color: transparent;
    border-left-color: var(--alert-accent);
    color: var(--ds-text-inverse);
}

.ds-toast--dark .ds-toast__close { color: var(--ds-text-inverse); }
.ds-toast--dark .ds-toast__close:hover { background: rgba(250, 249, 252, .12); }

/* Text/Brand on this ground is 2.8:1. The frame draws no link on the
   dark card, so the link inverts and keeps its rule at rest rather
   than shipping an unreadable one. */
.ds-toast--dark .ds-toast__link {
    color: var(--ds-text-inverse);
    text-decoration: underline;
}

/* ---------- Dismissal ----------
   The X removes the node. The fade is this build's, not the
   frame's — a message that vanishes between frames is easy to miss. */

.ds-toast.is-leaving {
    opacity: 0;
    transition: opacity .18s ease;
}

@media (prefers-reduced-motion: reduce) {
    .ds-toast.is-leaving { transition: none; }
}

/* ============================================================
   COMPONENT: File upload
   ------------------------------------------------------------
   Ported from an SVG export of the Figma file-upload frame
   (270x229): a label, a 270x180 dashed dropzone holding a
   24px cloud, two lines of copy and a Button/Small, and an
   error row underneath.

   Almost none of this is new. The label, the 4px stack and the
   error row are the Input field's — the error glyph is the very
   same path the help row already ships, translated — so this
   file adds the dropzone and nothing else.
   ============================================================ */

:root {
    /* The only new token: the icon roles already name Primary,
       Secondary and Error, and the cloud is drawn in the brand. */
    --ds-icon-brand: #2797E8;

    --ds-corner-radius-4px: 4px;
}

/* ---------- Zone ----------
   180px tall as drawn, and the stack inside is not centred in it:
   20px above the cloud, 14px under the hint. Stated net of the
   1px border, as the Alert's are, because Figma straddles the
   bounds with a stroke where CSS puts it inside them. */

.ds-upload__zone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    padding: 19px 19px 13px;
    background: var(--ds-bg-background);
    border: 1px dashed var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-4px);
    text-align: center;
}

.ds-upload__icon {
    display: block;
    width: 24px;
    height: 24px;
    color: var(--ds-icon-brand);
}

.ds-upload__icon svg { display: block; width: 24px; height: 24px; }

/* The gaps are 10 / 4 / 8 / 8 — not one value, so they are stated
   on the parts rather than as a gap on the column. */

.ds-upload__prompt,
.ds-upload__or {
    margin: 0;
    font-size: 14px;      /* Body 2 */
    font-weight: 400;
    line-height: 1.5;
    color: var(--ds-text-primary);
}

.ds-upload__prompt { margin-top: 10px; }
.ds-upload__or { margin-top: 4px; }
.ds-upload__action { margin-top: 8px; }

/* Text/Primary, where the field's help row is Text/Secondary —
   see the port notes. */
.ds-upload__hint {
    margin: 8px 0 0;
    font-size: 12px;      /* Body 3 */
    font-weight: 400;
    line-height: 1.5;
    color: var(--ds-text-primary);
}

/* ---------- The input ----------
   Hidden but focusable, with the visible control a <label for> so
   it is reachable and operable from the keyboard. */

.ds-upload__input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.ds-upload__input:focus-visible ~ .ds-upload__action {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* ---------- Drag-over ----------
   Not drawn. A dropzone that does not answer a dragged file is
   the one state it cannot ship without, so it is this build's:
   the outline moves to Outline/Brand over the brand's soft ground,
   both already in the library. */

.ds-upload.is-dragover .ds-upload__zone {
    border-color: var(--ds-outline-brand);
    background: var(--ds-bg-brand-soft);
}

/* ---------- Error ----------
   The frame leaves the dashed outline in Outline/Tertiary with an
   error underneath it. Reproduced; see the port notes. */

/* ---------- The composite ----------
   Not drawn. The dropzone frame and the row frame are separate in
   Figma with nothing between them, so the join is this build's:
   16px under the zone, and the rows at the 10px the row frame
   spaces its own three cards by.

   Single hides the zone while it holds a file — one file means the
   box has nothing left to offer — and shows it again when the row
   is deleted. The label above the zone is not hidden with it: it
   names the field, not the box. */

.ds-uploader {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: 100%;
}

.ds-uploader__list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* An empty list must not spend the 16px gap above it. The markup
   emits it on one line so there is no text node to defeat :empty. */
.ds-uploader__list:empty { display: none; }

.ds-uploader--single.is-filled .ds-upload__zone { display: none; }

/* ============================================================
   COMPONENT: File item
   ------------------------------------------------------------
   Ported from an SVG export of the file-row frame (534x399):
   three states of the row that follows the dropzone —
   uploading, uploaded, failed.

   It is a 24px file glyph, a column holding the name, a meta
   line, a progress bar and two link buttons, and a pair of
   24px icon buttons at the far end. The error row is the Input
   field's help row for the third frame running.
   ============================================================ */

:root {
    --ds-icon-success: #47D2A5;
}

/* The paddings are stated net of the border and they are not
   symmetric: 20 on the left against 12 on the right, which is what
   puts the 354px column between a 24px glyph and a 60px action
   group inside a 494px card. */
.ds-fileitem {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    width: 100%;
    padding: 11px 11px 11px 19px;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    font-family: var(--ds-font);
    color: var(--ds-text-primary);
}

/* Every glyph box in the row is top-aligned 20px from the card's
   edge, which is 8px below the top of the text column. The action
   buttons carry 4px of their own padding, so they need 4. */
.ds-fileitem__icon {
    flex: 0 0 24px;
    width: 24px;
    height: 24px;
    margin-top: 8px;
    color: var(--ds-icon-secondary);
}

.ds-fileitem__icon svg { display: block; width: 24px; height: 24px; }

.ds-fileitem__body { flex: 1 1 auto; min-width: 0; }

/* Not drawn: the frame has one short name. A row that cannot be
   given a long file name is not a row. */
.ds-fileitem__name {
    margin: 0;
    font-size: 14px;      /* Body 2 */
    font-weight: 400;
    line-height: 21px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 2px — the one gap in this frame that is not on the scale. */
.ds-fileitem__meta {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 2px 0 0;   /* the row is a <p>; its UA margin would add 14px under it */
    font-size: 14px;
    line-height: 21px;
    color: var(--ds-text-secondary);
}

.ds-fileitem__status {
    display: block;
    flex: 0 0 16px;
    width: 16px;
    height: 16px;
    color: var(--ds-icon-success);
}

.ds-fileitem__status svg { display: block; width: 16px; height: 16px; }

.ds-fileitem__percent { margin-left: auto; white-space: nowrap; }

/* ---------- Progress ---------- */

.ds-fileitem__bar {
    margin-top: 4px;
    height: 4px;
    border-radius: 2px;
    background: var(--ds-outline-tertiary);
    overflow: hidden;
}

.ds-fileitem__bar span {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: var(--ds-bg-brand);
    transition: width .2s linear;
}

.ds-fileitem--done .ds-fileitem__bar span { background: var(--ds-bg-success); }
.ds-fileitem--failed .ds-fileitem__bar span { background: var(--ds-bg-error); }

@media (prefers-reduced-motion: reduce) {
    .ds-fileitem__bar span { transition: none; }
}

/* The Input field's help row, again unchanged. */
.ds-fileitem .ds-field__help { margin-top: 4px; }

/* ---------- Links ----------
   Button/Link at Small. The frame gives the row a 21px line where
   the Buttons frame draws a 20px box, so the label carries the
   line-height and the button follows it. */

.ds-fileitem__links { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 8px; }
.ds-fileitem__links .ds-btn--link { min-height: 21px; }
.ds-fileitem__links .ds-btn__label { line-height: 21px; }

/* ---------- Actions ----------
   Two 24px slots and a 12px gap, held at 60px whether both are
   filled or not: the frame draws the same 354px bar in all three
   states, and a list whose bars change length as rows succeed and
   fail would be worse than the reserved space. */

.ds-fileitem__actions {
    display: flex;
    flex: 0 0 60px;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 4px;
}

/* ---------- List ----------
   The export stacks the three cards 10px apart. */
.ds-fileitem + .ds-fileitem { margin-top: 10px; }

/* ============================================================
   COMPONENT: Modal
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Modals" frame: five
   cards at 375, 400, 500, 800 and 940 wide.

   Four of them are one dialog at four widths, all 364px tall,
   and the parts close on that number exactly —

       56 header + 16 + 200 content + 16 + 16 + 44 buttons + 16

   — which is what makes the gap under the content two paddings
   that meet rather than one 32px rule of its own.

   The fifth, at 375, changes five things at once: 12px insets,
   square bottom corners, a rule above the footer, full-width
   buttons and the primary moved to the top of the stack. That is
   a breakpoint, not a size, so it lives here as one block of
   custom properties — applied by .ds-modal--sheet, and again by
   the media query at the bottom without anyone asking.
   ============================================================ */

:root {
    /* The frame's drop shadow: y 10, blur 30, #3E134D at 47% —
       five times the Alert's and seven times the Droplist's. */
    --ds-elevation-modal: 0 10px 30px rgba(62, 19, 77, .47);

    /* Not drawn. The frame floats its cards on nothing, so the
       ground behind them is this build's: the shadow's own ink. */
    --ds-scrim: rgba(62, 19, 77, .4);
}

.ds-modal {
    /* The five values the sheet form swaps out, and nothing else */
    --modal-pad: 16px;
    --modal-gap: 12px;
    --modal-btn-min: 134px;
    --modal-rule: 0;
    --modal-fill: 200px;

    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 500px;
    /* Never taller than what it floats in; the body takes the scroll */
    max-height: calc(100vh - 48px);
    background: var(--ds-bg-background);
    border-radius: var(--ds-corner-radius-8px);
    box-shadow: var(--ds-elevation-modal);
    font-family: var(--ds-font);
    /* So the header rule and the footer stay inside the 8px corner */
    overflow: hidden;
}

.ds-modal--sm { max-width: 400px; }
.ds-modal--md { max-width: 500px; }
.ds-modal--lg { max-width: 800px; }
.ds-modal--xl { max-width: 940px; }

/* ---------- Header ----------
   56px, and the only region whose height the frame fixes. The rule
   is Figma's 1px line on the bounds; border-box keeps the box at 56
   with the border inside it rather than 57 with it hanging off. */

.ds-modal__header {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 8px;
    height: 56px;
    padding: 0 var(--modal-pad);
    border-bottom: 1px solid var(--ds-outline-tertiary);
}

.ds-modal__title {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: 16px;      /* Body 1 */
    font-weight: 600;     /* SemiBold — the stems measure 0.14em */
    line-height: 24px;
    color: var(--ds-text-primary);
    /* Not drawn: the frame's title is one short word. One line and an
       ellipsis, so a long one cannot push the close off the edge. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Both header glyphs are 20px stroked at 1.5, where the Icon buttons
   frame strokes its 20px copies at 1 and saves 1.5 for the 24s. The
   two frames disagree; this one renders as drawn. */
.ds-modal__header .ds-btn__icon svg { stroke-width: 1.5; }

/* The close is drawn as a bare glyph with its right edge on the
   padding line — no box at all. It is rendered as a ghost Icon
   button so it has a target and a hover, and pulled back by the 6px
   the 32px box adds around a 20px glyph, so at rest the glyph sits
   exactly where the export puts it. */
.ds-modal__close { margin-right: -6px; }

/* ---------- Body ----------
   The component owns the frame, not what goes in it. */

.ds-modal__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--modal-pad);
    font-size: 16px;      /* Body 1 */
    line-height: 24px;
    color: var(--ds-text-primary);
}

/* The grey block the frame draws in place of content: 200px tall on
   the four dialogs, 44 on the sheet, square corners, one centred
   line. Text/Secondary on this ground is 3.40:1 — placeholder copy,
   and not a value to set real text in. */
.ds-modal__placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--modal-fill);
    /* 10px so the minimum lands on the drawn 44 around one 24px line,
       and long copy still clears the edges. */
    padding: 10px 16px;
    background: var(--ds-bg-grayscale-b);
    color: var(--ds-text-secondary);
    text-align: center;
}

/* ---------- Footer ----------
   Right-aligned, 12px apart. The rule is drawn on the sheet only,
   and the modifier puts it on a dialog that scrolls. */

.ds-modal__footer {
    display: flex;
    flex: 0 0 auto;
    flex-direction: row;
    justify-content: flex-end;
    gap: var(--modal-gap);
    padding: var(--modal-pad);
    border-top: var(--modal-rule) solid var(--ds-outline-tertiary);
}

.ds-modal--rule { --modal-rule: 1px; }

/* Both buttons are drawn at 134px around 84.5px of ink, where
   Button/Large hugs to about 124.5. Held as a minimum rather than a
   width: a pair of short labels stays matched, a long one still
   grows. */
.ds-modal__footer .ds-btn { min-width: var(--modal-btn-min); }

/* ---------- Sheet ----------
   The 375px card. One block, applied twice — by the modifier, for
   showing it deliberately on a wide screen, and by the media query
   at the bottom of this section for every card on a phone. */

.ds-modal--sheet {
    --modal-pad: 12px;
    --modal-gap: 8px;
    --modal-btn-min: 100%;
    --modal-rule: 1px;
    --modal-fill: 44px;

    max-width: none;
    max-height: calc(100vh - 24px);
    /* Anchored to the bottom edge, so only the top corners are cut */
    border-radius: var(--ds-corner-radius-8px) var(--ds-corner-radius-8px) 0 0;
}

/* The primary is on top of the stack and still second in the markup:
   reversing the column keeps the tab order the same in both forms. */
.ds-modal--sheet .ds-modal__footer { flex-direction: column-reverse; }

/* ---------- The live one ----------
   None of this is in the frame. The ground behind the card, the
   120ms it takes to arrive and where it arrives from are all this
   build's. */

.ds-modal-scrim {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: var(--ds-scrim);
    animation: ds-modal-ground .12s ease-out;
}

.ds-modal-scrim .ds-modal { animation: ds-modal-in .12s ease-out; }

.ds-modal-scrim--sheet {
    align-items: flex-end;
    justify-content: stretch;
    padding: 0;
}

.ds-modal-scrim--sheet .ds-modal { animation: ds-modal-up .12s ease-out; }

@keyframes ds-modal-ground { from { opacity: 0; } }
@keyframes ds-modal-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes ds-modal-up { from { transform: translateY(16px); } }

@media (prefers-reduced-motion: reduce) {
    .ds-modal-scrim,
    .ds-modal-scrim .ds-modal { animation: none; }
}

/* ---------- Below 768px ----------
   Identical to .ds-modal--sheet above. Any of the four widths
   becomes the drawn 375px card on a phone without being asked. */

@media (max-width: 767px) {
    .ds-modal {
        --modal-pad: 12px;
        --modal-gap: 8px;
        --modal-btn-min: 100%;
        --modal-rule: 1px;
        --modal-fill: 44px;

        max-width: none;
        max-height: calc(100vh - 24px);
        border-radius: var(--ds-corner-radius-8px) var(--ds-corner-radius-8px) 0 0;
    }

    .ds-modal__footer { flex-direction: column-reverse; }

    .ds-modal-scrim {
        align-items: flex-end;
        justify-content: stretch;
        padding: 0;
    }
}

/* ---------- The explorer's stage ----------
   Up to 940 wide against the 407 a form control gets, so it names
   that width in its definition — playWidth: '940px'. The one-off
   rule that used to widen the stage from here has gone with it. */

/* A sheet is as wide as the screen it is on. In here that would be
   the explorer's own column, so it is held at the 375 drawn. */
.dsx-stage .ds-modal--sheet,
.dsx-specimen .ds-modal--sheet { max-width: 375px; }
.dsx-specimens:has(.ds-modal) { grid-template-columns: 1fr; }

/* ============================================================
   COMPONENT: Tooltip
   ------------------------------------------------------------
   Ported from an SVG export of the Figma "Tooltip" frame
   (296x866): sixteen bubbles — two tones x two sizes x four
   tail placements.

   One row: glyph, copy, glyph, inside 16px of padding. The
   height is the glyph's rather than the text's — 8 + 24 + 8
   comes to exactly 40, and 8 + 20 + 8 to exactly 36 — so it is
   written as a stack and not as a fixed height.

   The tail is the only new drawing. In the file its outline is
   faked with three 1px offset shadows; here it is one open SVG
   path, stroked on the two slanted edges only, so its base can
   sit over the card's own border with nothing to hide.
   ============================================================ */

:root {
    /* y 6, blur 18, black at 8% — the only lift in this library
       that is not #3E134D. See the port notes. */
    --ds-elevation-tooltip: 0 6px 18px rgba(0, 0, 0, .08);
}

.ds-tooltip {
    --tt-ground: var(--ds-bg-background);
    --tt-border: var(--ds-outline-tertiary);
    --tt-ink: var(--ds-text-primary);

    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    /* 8 and 16 as drawn, one pixel of each given to the border so
       the box lands on 40 either way — the dark tone keeps the
       border and makes it transparent rather than dropping it. */
    padding: 7px 15px;
    max-width: 280px;
    background: var(--tt-ground);
    border: 1px solid var(--tt-border);
    border-radius: var(--ds-corner-radius-8px);
    box-shadow: var(--ds-elevation-tooltip);
    font-family: var(--ds-font);
    color: var(--tt-ink);
}

.ds-tooltip--dark {
    --tt-ground: var(--ds-bg-inverse);
    --tt-border: transparent;
    --tt-ink: var(--ds-text-inverse);
}

.ds-tooltip__label { margin: 0; }

.ds-tooltip--lg .ds-tooltip__label { font-size: 14px; line-height: 21px; }  /* Body 2 */
.ds-tooltip--sm .ds-tooltip__label { font-size: 12px; line-height: 18px; }  /* Body 3 */

/* The glyph box is what sets the height: 24 in a 40px card, 20 in
   a 36px one. Turn both off and the card is 37 / 34. */
.ds-tooltip__icon { display: flex; flex: 0 0 auto; }
.ds-tooltip__icon svg { display: block; }
.ds-tooltip--lg .ds-tooltip__icon svg { width: 24px; height: 24px; }
.ds-tooltip--sm .ds-tooltip__icon svg { width: 20px; height: 20px; }

/* ---------- Tail ----------
   14 x 6, five pixels clear of the card with its base one pixel
   inside it. The 1px comes free: an absolutely positioned child is
   placed against the padding box, so `bottom: 100%` already lands
   the base on the inner edge of the border, exactly where the
   export puts it. */

.ds-tooltip__tail {
    position: absolute;
    display: block;
    line-height: 0;
    pointer-events: none;
}

.ds-tooltip__tail svg { display: block; }

.ds-tooltip__tail path {
    fill: var(--tt-ground);
    stroke: var(--tt-border);
    stroke-width: 1;
}

.ds-tooltip--top .ds-tooltip__tail {
    bottom: 100%;
    left: 50%;
    margin: 0 0 0 -7px;
}

.ds-tooltip--bottom .ds-tooltip__tail {
    top: 100%;
    left: 50%;
    margin: 0 0 0 -7px;
}

.ds-tooltip--left .ds-tooltip__tail {
    right: 100%;
    top: 50%;
    margin: -7px 0 0 0;
}

.ds-tooltip--right .ds-tooltip__tail {
    left: 100%;
    top: 50%;
    margin: -7px 0 0 0;
}

/* ---------- On a trigger ----------
   Not drawn: the frame has the bubble and nothing it belongs to.
   Hover and :focus-within, so it answers the keyboard too, and
   8px of offset — which puts the tip of the tail 3px off the
   thing it points at. */

.ds-tooltip-anchor {
    position: relative;
    display: inline-flex;
}

.ds-tooltip-anchor > .ds-tooltip {
    position: absolute;
    z-index: 20;
    /* An absolutely positioned box shrinks to fit its containing
       block, which here is the trigger — so a 40px icon button would
       wrap the copy into a column. max-content restores the hug and
       lets the max-width above do the wrapping instead. */
    width: max-content;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity .12s ease, visibility .12s;
}

.ds-tooltip-anchor:hover > .ds-tooltip,
.ds-tooltip-anchor:focus-within > .ds-tooltip {
    opacity: 1;
    visibility: visible;
}

/* The modifier names the edge the tail is on, which is what the
   frame draws — so the bubble lands on the opposite side of the
   trigger from its own tail. Tail on top means the bubble is
   underneath. */

.ds-tooltip-anchor > .ds-tooltip--top {
    top: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
}

.ds-tooltip-anchor > .ds-tooltip--bottom {
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
}

.ds-tooltip-anchor > .ds-tooltip--left {
    left: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
}

.ds-tooltip-anchor > .ds-tooltip--right {
    right: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
}

@media (prefers-reduced-motion: reduce) {
    .ds-tooltip-anchor > .ds-tooltip { transition: none; }
}

/* The gallery cells clip nothing, so a tooltip anchored inside one
   has room for its bubble on any of the four sides. */
.dsx-specimens:has(.ds-tooltip-anchor) .dsx-specimen { padding: 40px var(--ds-4); }

/* ============================================================
   FOUNDATIONS: Colour
   ------------------------------------------------------------
   The combination checker: one composite carrying a text, an
   icon and an outline value over a ground, so three colours can
   be judged where they actually meet. Values live in the JS,
   where the contrast maths can reach them; this block only
   draws the box.
   ============================================================ */

/* ---------- Combination preview ----------
   Geometry matches Input field Large — 44px, 8px radius, 12px
   padding, 24px glyph — so what is judged is what is built. */

.ds-combo {
    padding: 20px;
    background: var(--combo-ground);
    border-radius: var(--ds-corner-radius-8px);
}

.ds-combo__field {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 44px;
    padding: 0 11px;
    border: 1px solid var(--combo-outline);
    border-radius: var(--ds-corner-radius-8px);
}

.ds-combo__value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--combo-text);
    font-family: var(--ds-font);
    line-height: 1.5;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ds-combo__icon {
    flex: 0 0 24px;
    width: 24px;
    height: 24px;
    color: var(--combo-icon);
}

.ds-combo__icon svg { display: block; width: 100%; height: 100%; }

/* min() on the track floor: a bare minmax(180px, ...) cannot shrink
   below 180, so on a narrow screen two columns of it pushed the whole
   page wider than the window rather than collapsing to one. */
.ds-combo__readout {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
    gap: 8px 16px;
    margin: 12px 0 0;
}

.ds-combo__readout dt {
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ds-text-disabled);
}

.ds-combo__readout dd {
    margin: 2px 0 0;
    font-size: 12px;
    color: var(--ds-text-secondary);
}

.ds-combo__bar { display: block; font-size: 11px; color: var(--ds-text-disabled); }




.ds-verdict {
    font-variant-numeric: tabular-nums;
    font-weight: 500;
}

.ds-verdict.is-pass { color: #1B7A5A; }
.ds-verdict.is-fail { color: var(--ds-text-error); }


/* ============================================================
   FOUNDATIONS: Typography
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame
   "Typography": thirteen scale categories over five columns
   (Scale Category, Size, Line Height, Weight, Typeface).

   One shorthand token per category, in the frame's own order.
   Apply with `font: var(--ds-body-1)`. Line height is kept as
   the drawn percentage, unitless, so a role sets the same
   proportion whatever it is nested in — and so the token can be
   read back against the frame without arithmetic.

   Three weights, three sizes, one family. The frame names the
   family "Poppin"; it is Poppins, already imported at the top
   of this file.
   ============================================================ */

:root {
    /* The weight names Poppins itself ships under */
    --ds-font-weight-regular: 400;
    --ds-font-weight-medium: 500;
    --ds-font-weight-semibold: 600;

    /* Title — SemiBold. 125% on the top two, 150% below.
       The frame draws 128%, which is the one line height in the
       scale that does not resolve to a whole pixel: 51.2px and
       40.96px. 125% lands them on 50 and 40 and keeps the intent
       — the two largest sizes set tighter than everything else. */
    --ds-title-1: var(--ds-font-weight-semibold) 40px/1.25 var(--ds-font);
    --ds-title-2: var(--ds-font-weight-semibold) 32px/1.25 var(--ds-font);
    --ds-title-3: var(--ds-font-weight-semibold) 24px/1.5 var(--ds-font);
    --ds-title-4: var(--ds-font-weight-semibold) 18px/1.5 var(--ds-font);

    /* Subtitle — Medium */
    --ds-subtitle-1: var(--ds-font-weight-medium) 16px/1.5 var(--ds-font);
    --ds-subtitle-2: var(--ds-font-weight-medium) 14px/1.5 var(--ds-font);
    --ds-subtitle-3: var(--ds-font-weight-medium) 12px/1.5 var(--ds-font);

    /* Body — Regular */
    --ds-body-1: var(--ds-font-weight-regular) 16px/1.5 var(--ds-font);
    --ds-body-2: var(--ds-font-weight-regular) 14px/1.5 var(--ds-font);
    --ds-body-3: var(--ds-font-weight-regular) 12px/1.5 var(--ds-font);

    /* Button — Medium. Value for value these are Subtitle 1-3;
       the names are kept apart because the intent is, and a
       change to one has to be a decision about the other. */
    --ds-button-1: var(--ds-font-weight-medium) 16px/1.5 var(--ds-font);
    --ds-button-2: var(--ds-font-weight-medium) 14px/1.5 var(--ds-font);
    --ds-button-3: var(--ds-font-weight-medium) 12px/1.5 var(--ds-font);
}

/* ---------- Mobile ----------
   Below 768px the four display sizes come down and nothing else
   moves. Body, Subtitle and Button hold their desktop values on
   purpose: 16px is the floor for comfortable reading, iOS zooms
   the page on any input under it, and a narrow screen is the last
   place to make text smaller.

   The titles are the real problem. A 375px viewport less its
   gutters leaves about 343px of measure, and Title 1 at 40px puts
   roughly fifteen characters on a line — a real headline breaks
   into three. At 28px it holds about twenty-two, which is inside
   the 20-30 a headline wants.

   Title 4 stays at 18px. It already fits, and taking it to 16
   would land it on Subtitle 1 with only a weight between them.

   Every line box here is a whole pixel, and the top two keep the
   tighter setting the desktop scale gives them.

   Same 767px breakpoint the modal sheet uses, so the library has
   one breakpoint rather than two. */

@media (max-width: 767px) {
    :root {
        --ds-title-1: var(--ds-font-weight-semibold) 28px/1.25 var(--ds-font);  /* 35px */
        --ds-title-2: var(--ds-font-weight-semibold) 24px/1.25 var(--ds-font);  /* 30px */
        --ds-title-3: var(--ds-font-weight-semibold) 20px/1.5 var(--ds-font);   /* 30px */
    }
}

/* ---------- Utilities ----------
   One class per category, so markup can name the role rather
   than restate three values. Colour is not set here: what a
   role is worth in ink is the Colour foundation's business. */

.ds-title-1 { font: var(--ds-title-1); }
.ds-title-2 { font: var(--ds-title-2); }
.ds-title-3 { font: var(--ds-title-3); }
.ds-title-4 { font: var(--ds-title-4); }
.ds-subtitle-1 { font: var(--ds-subtitle-1); }
.ds-subtitle-2 { font: var(--ds-subtitle-2); }
.ds-subtitle-3 { font: var(--ds-subtitle-3); }
.ds-body-1 { font: var(--ds-body-1); }
.ds-body-2 { font: var(--ds-body-2); }
.ds-body-3 { font: var(--ds-body-3); }
.ds-button-1 { font: var(--ds-button-1); }
.ds-button-2 { font: var(--ds-button-2); }
.ds-button-3 { font: var(--ds-button-3); }

/* ---------- Specimen ----------
   The explorer's sample line, not a shipped part: the role class
   next to it is doing all the work. */

.ds-type__sample {
    margin: 0;
    color: var(--ds-text-primary);
    overflow-wrap: anywhere;
}

/* A line of type wants the full measure, not a 376px grid cell. */
.dsx-specimens:has(.ds-type__sample) { grid-template-columns: minmax(0, 1fr); }


/* ============================================================
   COMPONENT: Inline message
   ------------------------------------------------------------
   Ported from Figma — the "inline message" frame: three cards,
   297x70, one per tone, in a dashed section wrapper.

   The Alert's quieter relative, and measurably so. Same three
   glyphs, but the tone leaves the 5px rule and colours the card
   itself; the elevation, the outline and the close all go; and
   what was a title with a link beside it is a message with a
   description under it.

   Everything below is read off the export. Card 297x70 at 8px
   radius, 12px padding all round, 12px between the glyph frame
   and the text, 4px between the two lines. The glyph is 16px
   centred in the library's 24px icon frame, which is what puts
   its top edge 16px from the card's — 4px below the text block,
   exactly as drawn.
   ============================================================ */

:root {
    /* Bg — brand and error already had a soft ground. Warning did
       not; #FFF5F1 is the value the frame fills the third card
       with, and it had no name anywhere in this file. */
    --ds-bg-warning-soft: #FFF5F1;
}

.ds-inline-msg {
    --msg-ground: var(--ds-bg-brand-soft);
    --msg-glyph: var(--ds-bg-brand);

    display: flex;
    align-items: flex-start;
    gap: 12px;
    width: 100%;
    padding: 12px;
    border-radius: var(--ds-corner-radius-8px);
    background: var(--msg-ground);
    font-family: var(--ds-font);
    color: var(--ds-text-primary);
}

/* ---------- Tones ----------
   Two lines each: the ground and the glyph. Nothing else in the
   card changes colour — not the rule, because there isn't one,
   and not the copy, which stays Text/Primary in all three. */

.ds-inline-msg--info    { --msg-ground: var(--ds-bg-brand-soft);   --msg-glyph: var(--ds-bg-brand); }
.ds-inline-msg--warning { --msg-ground: var(--ds-bg-warning-soft); --msg-glyph: var(--ds-bg-warning); }
.ds-inline-msg--error   { --msg-ground: var(--ds-bg-error-soft);   --msg-glyph: var(--ds-bg-error); }

/* ---------- Glyph ----------
   The 24px frame is load-bearing: it is the 4px of air above and
   left of the 16px mark. Drawn fill-only, matching Alert — the
   export also strokes the triangle and the hexagon in their own
   fill colour, which fattens each by 1px. See the notes. */

.ds-inline-msg__icon {
    display: flex;
    flex: 0 0 24px;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    color: var(--msg-glyph);
}

.ds-inline-msg__glyph { display: block; width: 16px; height: 16px; }
.ds-inline-msg__glyph-shape { fill: currentColor; }

.ds-inline-msg__glyph-mark {
    fill: none;
    stroke: var(--ds-bg-grayscale-a);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ---------- Body ---------- */

.ds-inline-msg__body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: 4px;
    min-width: 0;
}

/* 14px SemiBold is not a row in the type scale — the same
   off-scale pairing .ds-toast__title ships. Stated in longhand
   rather than a token because no token says it. */
.ds-inline-msg__title {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    line-height: 21px;
}

/* Subtitle 2 exactly — 14 / Medium / 150%. The stems measure
   0.091em against the message's 0.114em. */
.ds-inline-msg__desc {
    margin: 0;
    font: var(--ds-subtitle-2);
}

/* ============================================================
   COMPONENT: Code
   ------------------------------------------------------------
   Ported from Figma — "Chuong Design System", frame "Code"
   (SVG export, 590x772).

   One box per character, sitting on the help row Input / Standard
   already owns: .ds-field__help, its glyph insets and its 12px /
   10px sizes all come from that block above and are not repeated.
   What is new is the row of boxes, the 8px it keeps from the help
   text, and the weight — see the last rule in this block.
   ============================================================ */

/* .ds-field stacks its parts on a 4px gap. This frame draws 8px
   between the boxes and the help row at both sizes. */
.ds-code { gap: 8px; }

.ds-code__cells {
    display: flex;
    align-items: center;
}

.ds-field--lg .ds-code__cells { gap: 8px; }
.ds-field--sm .ds-code__cells { gap: 4px; }

/* ---------- Box ----------
   Square, and fixed: the boxes do not stretch to fill the line the
   way .ds-field__control does, because a code is a known length and
   a 44px box is exactly the touch target. */

.ds-code__cell {
    box-sizing: border-box;
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    font-family: var(--ds-font);
    text-align: center;
    color: var(--ds-text-primary);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
    outline: none;
    appearance: none;
}

.ds-field--lg .ds-code__cell {
    width: 44px;
    height: 44px;
    font: var(--ds-subtitle-1);  /* 16 / Medium / 150% */
}

.ds-field--sm .ds-code__cell {
    width: 32px;
    height: 32px;
    font: var(--ds-subtitle-2);  /* 14 / Medium / 150% */
}

/* A specimen renders the character as a <span>, so the box has to
   centre it itself; an <input> is centred by text-align above. */
.ds-code__cell--static {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------- States ----------
   The cursor box takes Outline/Brand. `is-active` is the state as
   drawn; :focus is the same box in a live field. */

.ds-code__cell.is-active,
.ds-code__cell:focus {
    border-color: var(--ds-outline-brand);
}

/* Error paints every box, and the character stays neutral — the
   frame does not turn the digits red. */
.ds-code.ds-field--error .ds-code__cell,
.ds-code.ds-field--error .ds-code__cell:focus {
    border-color: var(--ds-outline-error);
}

/* Disabled: grey ground, outline unchanged, and only the character
   drops to 40%. Written as an alpha rather than `opacity`, which
   would take the border and the ground down with it. */
.ds-code.ds-field--disabled .ds-code__cell {
    background: var(--ds-bg-grayscale-a);
    border-color: var(--ds-outline-tertiary);
    color: rgba(61, 76, 102, .4);
    -webkit-text-fill-color: rgba(61, 76, 102, .4);
    cursor: not-allowed;
}

/* Input field dims its help row when disabled; this frame, like
   Dropdown and Multi-select, leaves it at full strength. The rule
   is inherited from .ds-field, so it is turned off here. */
.ds-code.ds-field--disabled .ds-field__help { opacity: 1; }

/* ---------- Resend ----------
   Proposed fidelity only. The frame draws the phrase as flat
   Text/Secondary with nothing to click; this is that phrase as the
   control it is, sized off the row it sits in so the line does not
   reflow when the toggle flips. */

.ds-code__resend {
    padding: 0;
    font: inherit;
    color: var(--ds-text-brand);
    background: none;
    border: 0;
    border-radius: 2px;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.ds-code__resend:hover { color: var(--ds-bg-brand-hover); }

.ds-code__resend:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* ---------- Weight ----------
   Both the character and the help text measure 0.091em in the stem
   — Medium 500. The shared .ds-field__help-text sets no weight and
   so renders Regular under Input field, Password and Search; this
   pins the frame's own value without moving those three. */
.ds-code .ds-field__help-text {
    font-weight: var(--ds-font-weight-medium);
}

/* ============================================================
   COMPONENT: Phone number
   ------------------------------------------------------------
   Ported from Figma — frame "Phone number" (SVG export): adjacent
   and floating label, each in a Large and a Small column, eight
   states per adjacent column and nine per floating one.

   Two boxes, 4px apart, under one label and one help row. The left
   is the Dropdown trigger; the right is the Input / Standard box.
   Both come in whole from those components, so everything below is
   the pairing and the two places the pair parts company with them.
   ============================================================ */

/* ---------- Row ----------
   align-items: start rather than stretch, so a wrapped help row or a
   notched label never stretches the dial-code box past 44px / 32px. */

.ds-phone__row {
    display: flex;
    align-items: flex-start;
    gap: 4px;
}

/* The frame fixes the dial-code box and lets the number box take what
   is left: 106 + 4 + 232 on Large, 82 + 4 + 256 on Small, so the pair
   measures 342px either way. */
.ds-phone__code { flex: 0 0 106px; }
.ds-field--sm .ds-phone__code { flex: 0 0 82px; }

.ds-field--phone .ds-phone__row > .ds-field__control {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
}

/* ---------- The seam ----------
   Focus and Error outline the number box alone — the dial-code box
   beside it holds Outline/Tertiary in every row the frame draws.
   Three classes deep, so it outranks the shared state rules without
   depending on where this block sits in the file. */

.ds-field--phone .ds-phone__code .ds-field__control {
    border-color: var(--ds-outline-tertiary);
}

/* Which leaves the picker with no focus of its own, so it takes the
   brand outline when it is the thing being used — the one state the
   static frame had no way to draw. Read-only is still focusable and
   still not editable, so it stays out. */
.ds-field--phone .ds-phone__code .ds-field:not(.ds-field--readonly) .ds-field__control:focus-within,
.ds-field--phone .ds-phone__code .ds-field--open .ds-field__control {
    border-color: var(--ds-outline-brand);
}

/* ---------- Menu ----------
   Anchored to a 106px trigger, the droplist would come out too narrow
   to read a dial code and a country in. It grows to the right instead. */

.ds-phone__code .ds-field__menu {
    right: auto;
    min-width: 220px;
}

/* ---------- Disabled ----------
   Input field dims its help row; this frame, like Dropdown,
   Multi-select and Code, leaves it at full strength. The rule is
   inherited from .ds-field, so it is turned off here. */

.ds-field--phone.ds-field--disabled .ds-field__help { opacity: 1; }

/* ============================================================
   COMPONENT: Badge
   ------------------------------------------------------------
   Ported from Figma — a 162x224 board holding five status chips
   (SVG export). One filled pill, a label, nothing else.

   Every fill was already a token. Two things were not: the 6px
   radius, and a soft Success ground for the proposed treatment.
   Both are added below, the same way the inline message added its
   warning ground.
   ============================================================ */

:root {
    /* The library rounds everything else at 8px. This frame rounds at
       6, so the second value gets a name rather than a magic number. */
    --ds-corner-radius-6px: 6px;

    /* Brand, Error and Warning all have a soft ground already; Success
       did not, because nothing had needed one. Same tint level as the
       other three — the hue at 95% lightness. */
    --ds-bg-success-soft: #EBFAF5;
}

.ds-badge {
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
    height: 24px;
    padding: 0 8px;
    font: var(--ds-subtitle-3);
    white-space: nowrap;
    background: var(--badge-fill);
    color: var(--badge-on);
    border-radius: var(--ds-corner-radius-6px);
}

/* ---------- Tones ----------
   As drawn: a saturated fill under Text/Inverse, except Neutral,
   which is the only one that does not put near-white on colour and
   the only one that clears the contrast floor. */

.ds-badge--success { --badge-fill: var(--ds-bg-success);      --badge-on: var(--ds-text-inverse); }
.ds-badge--error   { --badge-fill: var(--ds-bg-error);        --badge-on: var(--ds-text-inverse); }
.ds-badge--warning { --badge-fill: var(--ds-bg-warning);      --badge-on: var(--ds-text-inverse); }
.ds-badge--neutral { --badge-fill: var(--ds-bg-grayscale-b);  --badge-on: var(--ds-text-primary); }

/* ---------- Proposed ----------
   The contrast fix. Each tone drops to the soft ground it already
   owns and the label goes to Text/Primary, which is the inline
   message's treatment applied to a smaller box. Everything clears
   7:1. Neutral is unchanged — it was already the accessible one. */

.ds-badge--proposed { --badge-on: var(--ds-text-primary); }

.ds-badge--proposed.ds-badge--success { --badge-fill: var(--ds-bg-success-soft); }
.ds-badge--proposed.ds-badge--error   { --badge-fill: var(--ds-bg-error-soft); }
.ds-badge--proposed.ds-badge--warning { --badge-fill: var(--ds-bg-warning-soft); }


/* ============================================================
   COMPONENT: Table
   ------------------------------------------------------------
   Ported from two views of the same thing: an SVG export of the
   row frame (492 x 229), which is where every exact number comes
   from, and the Product List screen, which is where the eleven
   columns and their seven cell types come from.

   A column is a header, a width rule and one cell type. The types
   are mostly library components at the sizes the row frame draws
   them — Input/Small, Button/Small, Badge — so a change to those
   reaches the table. What is drawn here is the 48px image cell,
   the sort chevrons, the figure's dotted rule and the two-line
   note.
   ============================================================ */

.ds-table {
    position: relative;
    width: 100%;
    font-family: var(--ds-font);
}

/* Only the grid scrolls. The toolbar and the footer belong to the
   screen and stay put over it — which is also why eleven columns
   can push the actions off the right-hand edge with nothing in the
   chrome to say so. See the notes on the page. */
.ds-table__scroll {
    /* A containing block, so the visually-hidden labels inside the
       grid resolve against this box and are clipped by it. Without
       it they position against .ds-table, escape the scroller and
       stretch the page sideways at narrow widths. */
    position: relative;
    width: 100%;
    overflow-x: auto;
}

.ds-table__grid {
    width: 100%;
    border-collapse: collapse;
}

/* ---------- Rules ----------
   Three weights, all from the row frame: a hairline over the header
   in a background token, Outline/Secondary under it, and
   Outline/Tertiary under every row including the last. */

.ds-table__grid th {
    height: 32px;
    padding: 0 12px;
    text-align: left;
    vertical-align: middle;
    font: var(--ds-subtitle-2);
    color: var(--ds-text-primary);
    white-space: nowrap;
    border-top: 1px solid var(--ds-bg-grayscale-b);
    border-bottom: 1px solid var(--ds-outline-secondary);
}

.ds-table__grid td {
    height: 64px;
    padding: 0 12px;
    vertical-align: middle;
    border-bottom: 1px solid var(--ds-outline-tertiary);
}

/* The row frame's 12px margin, kept at both ends. */
.ds-table__grid th:first-child,
.ds-table__grid td:first-child { padding-left: 12px; }

.ds-table__grid th:last-child,
.ds-table__grid td:last-child { padding-right: 12px; }

/* ---------- Row states ----------
   The screen tints one row and draws no hover at all. Hover is
   added here because a 64px row eleven columns wide cannot be
   tracked across without one. */

.ds-table__body tr:hover td { background: var(--ds-bg-grayscale-a); }

.ds-table__body tr.is-selected td,
.ds-table__body tr.is-selected:hover td { background: var(--ds-bg-brand-soft); }

/* ---------- Columns ----------
   Every column takes its content's width; the note is the one that
   grows, so long copy has somewhere to go before it is clamped. */

/* ---------- Frozen columns ----------
   The two columns you can act on — the field you type into and the
   buttons — stay put while the rest scrolls under them. A row of
   eleven columns is wider than any screen, and an action you have to
   go looking for is an action that does not get taken.

   Both offsets come off one value: the actions column's width. It is
   stated rather than measured because a sticky column has to know how
   far from the edge it sits, and a column sized by its own button's
   label cannot answer that. */

.ds-table { --ds-table-actions-w: 136px; }

.ds-table__c--qty,
.ds-table__c--actions {
    position: sticky;
    z-index: 1;
    /* Content scrolls underneath, so these cannot be transparent. */
    background: var(--ds-bg-background);
}

/* min-width, not width: a width is a suggestion the auto table
   layout may shrink, and a frozen column narrower than the offset
   the next one uses opens a seam for content to scroll through. */
.ds-table__c--actions {
    right: 0;
    width: var(--ds-table-actions-w);
    min-width: var(--ds-table-actions-w);
}

.ds-table__c--qty { right: var(--ds-table-actions-w); }

/* With the actions column switched off, the field is the last thing
   in the row and sits against the edge itself. */
.ds-table__grid tr > .ds-table__c--qty:last-child { right: 0; }

/* The frozen pair carries its own left edge: without it the columns
   underneath appear to slide into nothing. */
.ds-table__grid tr > .ds-table__c--qty,
.ds-table__grid tr > .ds-table__c--actions:first-of-type {
    box-shadow: inset 1px 0 0 var(--ds-outline-tertiary), -6px 0 12px -8px rgba(61, 76, 102, .22);
}

.ds-table__grid tr > .ds-table__c--qty ~ .ds-table__c--actions { box-shadow: none; }

/* The row states have to reach the frozen cells too, or they stop
   at the freeze line. */
.ds-table__body tr:hover .ds-table__c--qty,
.ds-table__body tr:hover .ds-table__c--actions { background: var(--ds-bg-grayscale-a); }

.ds-table__body tr.is-selected .ds-table__c--qty,
.ds-table__body tr.is-selected .ds-table__c--actions,
.ds-table__body tr.is-selected:hover .ds-table__c--qty,
.ds-table__body tr.is-selected:hover .ds-table__c--actions { background: var(--ds-bg-brand-soft); }

.ds-table__c--media { min-width: 200px; }
.ds-table__c--qty   { width: 88px; }

.ds-table__t--text,
.ds-table__t--hint,
.ds-table__t--status,
.ds-table__t--actions { white-space: nowrap; }

/* ---------- Header: the sort control ---------- */

.ds-table__sort {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.ds-table__sort svg {
    display: block;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
}

/* One glyph, three states. Both chevrons rest at Outline/Secondary
   and the live one goes brand — colour carrying the whole message,
   which is why aria-sort and a written word go with it. */
.ds-table__chev { stroke: var(--ds-outline-secondary); }
.ds-table__sort.is-asc  .ds-table__chev--up { stroke: var(--ds-icon-brand); }
.ds-table__sort.is-desc .ds-table__chev--down { stroke: var(--ds-icon-brand); }

.ds-table__sort:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* ---------- Cell: image, title, description ---------- */

.ds-table__media {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.ds-table__thumb {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    width: 48px;
    height: 48px;
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

.ds-table__thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* 7px over an 8px line, as drawn. The scale stops at 12 — see the
   notes on the page. */
.ds-table__thumb-note {
    font: var(--ds-font-weight-medium) 7px/8px var(--ds-font);
    color: var(--ds-text-secondary);
    text-align: center;
}

.ds-table__text { min-width: 0; }

.ds-table__name,
.ds-table__detail {
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ds-table__name { font: var(--ds-body-2); }
.ds-table__detail { font: var(--ds-body-3); color: var(--ds-text-secondary); }

.ds-table__name a {
    color: var(--ds-text-brand);
    text-decoration: none;
}

.ds-table__name a:hover { text-decoration: underline; }

.ds-table__name a:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
    border-radius: 2px;
}

/* ---------- Cell: text ---------- */

.ds-table__value { font: var(--ds-body-2); color: var(--ds-text-primary); }

/* An answered-but-empty cell. Nothing at all reads as a bug. */
.ds-table__dash { font: var(--ds-body-2); color: var(--ds-text-secondary); }

/* ---------- Cell: figure with an explanation ----------
   The screen marks these with a dotted rule and nothing else. A
   button rather than a span, so the explanation is reachable from
   the keyboard as well as the pointer. */

.ds-table__hint {
    padding: 0;
    border: 0;
    border-bottom: 1px dotted var(--ds-outline-secondary);
    background: transparent;
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
    cursor: help;
}

.ds-table__hint:hover { border-bottom-color: var(--ds-outline-primary); }

.ds-table__hint:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* ---------- Cell: note ----------
   One line, cut with an ellipsis. On one line the copy has no
   reason to stop, so the width has to come from somewhere: the
   paragraph carries it rather than the cell, because a cell holding
   text that will not wrap sizes itself to the whole sentence and
   the column runs away. 260px is the column the screen draws. */

.ds-table__note {
    width: 260px;
    overflow: hidden;
    margin: 0;
    font: var(--ds-body-3);
    color: var(--ds-text-secondary);
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* ---------- Cell: field ---------- */

/* Input/Small is the one library component here with no width of
   its own, so the cell gives it the 88 the row frame draws. */
.ds-table__c--qty .ds-field { width: 88px; }

/* ---------- Cell: actions ---------- */

.ds-table__actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.ds-table__remove {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--ds-icon-secondary);
    cursor: pointer;
}

.ds-table__remove svg { display: block; width: 100%; height: 100%; }

.ds-table__remove:hover {
    background: var(--ds-bg-grayscale-b);
    color: var(--ds-icon-primary);
}

.ds-table__remove:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* The proposed target: the library's Large box, which is the 44px
   guideline and costs the row 20px of width. */
.ds-table__remove--lg {
    width: 44px;
    height: 44px;
    border-radius: var(--ds-corner-radius-8px);
}

.ds-table__remove--lg svg { width: 24px; height: 24px; }

/* ---------- Names for the unlabelled ---------- */

.ds-table__hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ============================================================
   Table — toolbar
   ------------------------------------------------------------
   Read off the Product List screen, which is a raster: the pieces
   are identifiable but their sizes are not measurable the way the
   row frame's are. Everything here is therefore a library
   component at the nearest library size, and the one number worth
   arguing about is in the notes — the screen's controls scale out
   closer to 40px than to the 44 used here, and 40 is not a size
   this library has.
   ============================================================ */

.ds-table__toolbar {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding: 0 0 16px;
}

.ds-table__toolbar-title {
    margin: 0;
    font: var(--ds-title-4);
    color: var(--ds-text-primary);
}

.ds-table__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 16px;
}

.ds-table__bar-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 16px;
    min-width: 0;
}

/* margin-left rather than space-between: when the bar wraps, the
   actions still sit at the right-hand end of their own line. */
.ds-table__bar-group--end { gap: 12px; margin-left: auto; }

/* The search is the only thing in the bar with a width of its own. */
.ds-table__search { flex: 0 1 320px; min-width: 180px; }

/* ---------- The two filters ----------
   A label, its current value and a chevron, with no box around any
   of it. Not a Dropdown — that component always draws its box —
   so this is the one control in the toolbar the library does not
   already own. */

.ds-table__filter {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 44px;
    margin: 0;
    padding: 0 4px;
    border: 0;
    border-radius: var(--ds-corner-radius-8px);
    background: transparent;
    font: var(--ds-body-2);
    color: var(--ds-text-secondary);
    white-space: nowrap;
    cursor: pointer;
}

.ds-table__filter-value {
    font: var(--ds-subtitle-2);
    color: var(--ds-text-primary);
}

.ds-table__filter-chevron {
    display: flex;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    color: var(--ds-icon-secondary);
}

.ds-table__filter-chevron svg { display: block; width: 100%; height: 100%; }

.ds-table__filter:hover { background: var(--ds-bg-grayscale-b); }

.ds-table__filter:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* ============================================================
   Table — footer
   ------------------------------------------------------------
   Page size, the count and the pager. That is Pagination, and
   Pagination is now built, so the footer is the strip it sits in
   and nothing else.
   ============================================================ */

.ds-table__foot { padding: 16px 0 0; }

/* ---------- In the explorer ----------
   The stage caps its content at 407px and the specimen gallery
   tiles at 376. This table is wider than any screen, so it takes
   the whole column and scrolls inside it — which needs min-width
   on every flex and grid ancestor between here and the page. */

.dsx-specimens:has(.ds-table) .dsx-specimen { min-width: 0; }

.dsx-specimens:has(.ds-table) { grid-template-columns: 1fr; }

/* ============================================================
   COMPONENT: Checkbox
   ------------------------------------------------------------
   Ported from two exported SVGs: the control with its label
   (117x102) and the states grid (92x98) — unchecked, checked and
   indeterminate, the first two repeated at the disabled treatment.

   The box stays white whether or not it is ticked; only a blue
   glyph appears inside it. The tick is the same path Droplist
   already carries, so it is written once here and both should use
   it — see the notes on the page for where Droplist's copy has
   drifted.
   ============================================================ */

:root {
    /* What the tickable controls share. Checkbox and Radio are the
       same 18px box, the same border, the same 8px to their label and
       the same fade when disabled — only the corner and the mark
       inside differ. Written once because a copy per component is
       exactly how Droplist's box drifted. */
    --ds-control-size: 18px;
    --ds-control-outline: var(--ds-outline-secondary);
    --ds-control-gap: 8px;

    /* The two glyphs, written once. Droplist draws the same control
       and used to carry its own copy of the tick, which is how its
       mixed dash came to be a different shape from this one. */
    --ds-check-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18'%3E%3Cpath d='M7.3964 12.582L4.1464 9.332C3.9512 9.137 3.9512 8.82 4.1464 8.625L4.8535 7.918C5.0488 7.723 5.3654 7.723 5.5606 7.918L7.75 10.107L12.4394 5.418C12.6346 5.223 12.9512 5.223 13.1465 5.418L13.8535 6.125C14.0488 6.32 14.0488 6.637 13.8535 6.832L8.1035 12.582C7.9083 12.777 7.5917 12.777 7.3964 12.582Z' fill='%232797E8'/%3E%3C/svg%3E");
    --ds-check-dash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18'%3E%3Crect x='4' y='8' width='10' height='2' fill='%232797E8'/%3E%3C/svg%3E");
}

.ds-check {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-control-gap);
    position: relative;
    font-family: var(--ds-font);
    cursor: pointer;
}

/* The real control, sat under the box that pictures it, so checked,
   disabled, the keyboard and the accessibility tree are the
   browser's job rather than this file's. */
.ds-check__input {
    position: absolute;
    width: var(--ds-control-size);
    height: var(--ds-control-size);
    margin: 0;
    opacity: 0;
}

.ds-check__box {
    position: relative;
    flex: 0 0 auto;
    width: var(--ds-control-size);
    height: var(--ds-control-size);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-control-outline);
    border-radius: 4px;
}

/* Checked keeps the white ground and the grey outline and drops the
   glyph inside — the export's own path, in the box's 18px
   coordinates. */
.ds-check__box::after {
    position: absolute;
    inset: -1px;
    content: "";
    background: no-repeat center / 18px 18px var(--ds-check-tick);
    opacity: 0;
}

.ds-check__input:checked + .ds-check__box::after { opacity: 1; }

/* Some on, some off. 10x2 with square ends, which is what the frame
   draws — Droplist's own dash is 9x1.5 and rounded, and the two
   should be reconciled. */
.ds-check__input:indeterminate + .ds-check__box::after,
.ds-check.is-mixed .ds-check__box::after {
    opacity: 1;
    background: no-repeat center / 18px 18px var(--ds-check-dash);
}

.ds-check__label {
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
}

/* ---------- States ----------
   Only default and disabled are drawn. Hover and focus are this
   build's: a control you click has to answer the pointer, and one
   you reach by keyboard has to show where you are. */

.ds-check:hover .ds-check__box,
.ds-check.is-hover .ds-check__box { border-color: var(--ds-outline-primary); }

.ds-check__input:focus-visible + .ds-check__box,
.ds-check.is-focus .ds-check__box {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

/* The frame puts 40% on the control and 60% on its label. One
   treatment or the other — a label more legible than the thing it
   names reads as a mistake. */
.ds-check:has(.ds-check__input:disabled) {
    opacity: var(--ds-opacity-disabled);
    cursor: not-allowed;
}

.ds-check:has(.ds-check__input:disabled):hover .ds-check__box {
    border-color: var(--ds-outline-secondary);
}

/* ============================================================
   COMPONENT: Radio
   ------------------------------------------------------------
   Ported from two exported SVGs: the states grid (128x85) —
   unselected and selected, each repeated at the disabled
   treatment — and the control with its label (117x102), drawn
   unselected at full strength and again faded.

   The same box as the Checkbox, rounded all the way and marked
   with a dot instead of a tick, so the size, the border, the gap
   to the label and the fade all come from the shared control
   tokens rather than being measured twice.
   ============================================================ */

.ds-radio {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-control-gap);
    position: relative;
    font-family: var(--ds-font);
    cursor: pointer;
}

.ds-radio__input {
    position: absolute;
    width: var(--ds-control-size);
    height: var(--ds-control-size);
    margin: 0;
    opacity: 0;
}

/* rx in the export is 8.5 on a 17px rect, which is half of it —
   a circle, not a rounded square. */
.ds-radio__box {
    position: relative;
    flex: 0 0 auto;
    width: var(--ds-control-size);
    height: var(--ds-control-size);
    background: var(--ds-bg-background);
    border: 1px solid var(--ds-control-outline);
    border-radius: 50%;
}

/* Selected keeps the white ground and the grey ring and puts a 12px
   dot inside — 2px of white left showing between the dot and the
   border, which is what the export draws. */
.ds-radio__box::after {
    position: absolute;
    inset: 2px;
    content: "";
    background: var(--ds-icon-brand);
    border-radius: 50%;
    opacity: 0;
}

.ds-radio__input:checked + .ds-radio__box::after { opacity: 1; }

.ds-radio__label {
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
}

/* ---------- States ----------
   Unselected and selected, each enabled and disabled: four boxes,
   which is the whole grid. Hover and focus are this build's, for the
   same reason they are on the Checkbox. */

.ds-radio:hover .ds-radio__box,
.ds-radio.is-hover .ds-radio__box { border-color: var(--ds-outline-primary); }

.ds-radio__input:focus-visible + .ds-radio__box,
.ds-radio.is-focus .ds-radio__box {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

.ds-radio:has(.ds-radio__input:disabled) {
    opacity: var(--ds-opacity-disabled);
    cursor: not-allowed;
}

.ds-radio:has(.ds-radio__input:disabled):hover .ds-radio__box {
    border-color: var(--ds-control-outline);
}

/* ============================================================
   COMPONENT: Toggle
   ------------------------------------------------------------
   Ported from the exported SVG (90x188): four tracks down one
   column — on and off, each repeated at the disabled treatment.

   A switch rather than a pill. The track is 36x20 with a 4px
   corner, which is the same radius the Checkbox uses, so the two
   read as the same family; most toggles round to a capsule and
   this deliberately does not.

   The knob's drop shadow decodes to the same y 8 / blur 16 /
   #3E134D at 7% the Droplist panel carries, so both read
   --ds-elevation-soft.
   ============================================================ */

.ds-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-control-gap);
    position: relative;
    font-family: var(--ds-font);
    cursor: pointer;
}

.ds-toggle__input {
    position: absolute;
    width: 36px;
    height: 20px;
    margin: 0;
    opacity: 0;
}

.ds-toggle__track {
    position: relative;
    flex: 0 0 auto;
    width: 36px;
    height: 20px;
    background: var(--ds-bg-grayscale-b);
    border-radius: 4px;
    transition: background .15s ease;
}

/* The export's own numbers. 1.6 top and bottom is exact; 1.566 at
   the ends and a knob 0.37 narrower than it is tall are not, and
   read as a component that was scaled — see the notes. */
.ds-toggle__knob {
    position: absolute;
    top: 1.6px;
    left: 1.566px;
    width: 16.4348px;
    height: 16.8px;
    background: var(--ds-bg-background);
    border-radius: 3px;
    box-shadow: var(--ds-elevation-soft);
    transition: left .15s ease;
}

.ds-toggle__input:checked + .ds-toggle__track { background: var(--ds-bg-brand); }

.ds-toggle__input:checked + .ds-toggle__track .ds-toggle__knob {
    left: calc(36px - 1.566px - 16.4348px);
}

.ds-toggle__label {
    font: var(--ds-body-2);
    color: var(--ds-text-primary);
}

/* ---------- States ----------
   On and off, each enabled and disabled: four tracks, which is the
   whole export. Hover and focus are this build's. */

.ds-toggle:hover .ds-toggle__track,
.ds-toggle.is-hover .ds-toggle__track { background: var(--ds-outline-tertiary); }

.ds-toggle:hover .ds-toggle__input:checked + .ds-toggle__track,
.ds-toggle.is-hover .ds-toggle__input:checked + .ds-toggle__track {
    background: var(--ds-bg-brand-hover);
}

.ds-toggle__input:focus-visible + .ds-toggle__track,
.ds-toggle.is-focus .ds-toggle__track {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
}

.ds-toggle:has(.ds-toggle__input:disabled) {
    opacity: var(--ds-opacity-disabled);
    cursor: not-allowed;
}

.ds-toggle:has(.ds-toggle__input:disabled):hover .ds-toggle__track {
    background: var(--ds-bg-grayscale-b);
}

.ds-toggle:has(.ds-toggle__input:disabled):hover .ds-toggle__input:checked + .ds-toggle__track {
    background: var(--ds-bg-brand);
}

/* ============================================================
   COMPONENT: Tabs
   ------------------------------------------------------------
   Ported from Figma — the "Tab bar" export (540x37, ten tabs)
   with its states sheet (93x221), and the unlabelled pill row
   (1138x32, ten pills). Two styles of one control.

   The rules under the underline style are inset shadows rather
   than borders. The frame's picked tab carries 2px and the rest
   carry 1px under labels that share a baseline, so a border would
   move the text by a pixel every time you picked a different tab.
   ============================================================ */

.ds-tabs {
    display: flex;
    align-items: stretch;
    /* Ten tabs is wider than most panes. The row scrolls; the
       component does not decide to hide any of them. */
    overflow-x: auto;
    scrollbar-width: thin;
}

.ds-tab {
    flex: 0 0 auto;
    margin: 0;
    background: transparent;
    border: 0;
    font-family: var(--ds-font);
    cursor: pointer;
    white-space: nowrap;
}

.ds-tab:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: -2px;
    border-radius: var(--ds-corner-radius-8px);
}

/* ---------- Underline ---------- */

.ds-tabs--underline .ds-tab {
    padding: 8px 12px;                  /* 8 + 21 + 8 = the 37 drawn */
    font: var(--ds-subtitle-2);
    color: var(--ds-text-secondary);
    border-radius: var(--ds-corner-radius-8px) var(--ds-corner-radius-8px) 0 0;
    box-shadow: inset 0 -1px 0 var(--ds-outline-tertiary);
}

.ds-tabs--underline .ds-tab.is-selected {
    color: var(--ds-text-primary);
    box-shadow: inset 0 -2px 0 var(--ds-bg-brand);
}

/* The ground the states sheet draws, on both a picked tab and an
   unpicked one. The rule underneath does not change. */
.ds-tabs--underline .ds-tab:hover,
.ds-tabs--underline .ds-tab.is-hover { background: var(--ds-bg-brand-soft); }

/* ---------- Pill ---------- */

.ds-tabs--pill {
    gap: 8px;
    padding-bottom: 2px;                /* room for the focus ring */
}

.ds-tabs--pill .ds-tab {
    height: 32px;
    padding: 0 12px;
    font: var(--ds-body-3);
    color: var(--ds-text-primary);
    background: transparent;
    border: 1px solid var(--ds-outline-tertiary);
    border-radius: var(--ds-corner-radius-8px);
}

.ds-tabs--pill .ds-tab.is-selected {
    color: var(--ds-text-brand);
    background: var(--ds-bg-brand-soft);
    border-color: var(--ds-bg-brand);
}

/* Not drawn. The pill has no rule under it to carry the pointer,
   so the border takes it. */
.ds-tabs--pill .ds-tab:hover,
.ds-tabs--pill .ds-tab.is-hover { border-color: var(--ds-outline-secondary); }

.ds-tabs--pill .ds-tab.is-selected:hover,
.ds-tabs--pill .ds-tab.is-selected.is-hover { border-color: var(--ds-bg-brand-hover); }

.ds-tabs--pill .ds-tab:focus-visible { outline-offset: 2px; }

/* ============================================================
   COMPONENT: Breadcrumb
   ------------------------------------------------------------
   Ported from Figma — the "Breadcrumb" export (330x18).

   58 from one label to the next resolves to a 16px separator box
   sitting flush against the label after it with 4px of air before
   it, so the chevron's ink lands 10 before the next crumb.
   ============================================================ */

.ds-breadcrumb { font: var(--ds-body-3); }

.ds-breadcrumb__list {
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0;
    list-style: none;
    /* A deep trail on a narrow pane scrolls rather than wraps: the
       order is the meaning, and a wrapped trail loses it. */
    overflow-x: auto;
    scrollbar-width: thin;
}

.ds-breadcrumb__item {
    display: flex;
    align-items: center;
    min-width: 0;
}

.ds-breadcrumb__link {
    color: var(--ds-text-primary);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Not drawn. Six labels in one treatment leave nothing saying
   which of them go anywhere. */
a.ds-breadcrumb__link:hover { text-decoration: underline; }

a.ds-breadcrumb__link:focus-visible {
    outline: 2px solid var(--ds-outline-brand);
    outline-offset: 2px;
    border-radius: 2px;
}

.ds-breadcrumb__current {
    color: var(--ds-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ds-breadcrumb__sep {
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    margin-left: 4px;
    color: var(--ds-outline-secondary);
}

.ds-breadcrumb__sep svg { width: 16px; height: 16px; }

/* ============================================================
   COMPONENT: Pagination
   ------------------------------------------------------------
   Ported from Figma — the "Pagination" export (1020x223): four
   page sizes, the select flush left and the pager flush right,
   with the whole width between them.
   ============================================================ */

.ds-pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}

/* The one control in the frame drawn at Small. 140 as measured. */
.ds-pagination__size .ds-field { width: 140px; }

.ds-pagination__end {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-left: auto;
}

.ds-pagination__count {
    margin: 0;
    font: var(--ds-body-2);
    color: var(--ds-text-secondary);
    white-space: nowrap;
}

/* 4 between the two in a pair, 16 between the pairs — the gap in
   the middle is where the page numbers would be. */
.ds-pager {
    display: flex;
    align-items: center;
    gap: 4px;
}

.ds-pager__btn:nth-child(3) { margin-left: 12px; }

/* The frame pairs a 24px glyph with the 32px box, which is not a
   pairing the icon button offers. Scoped to here rather than
   loosened across the library. */
.ds-pager .ds-iconbtn--md .ds-btn__icon svg {
    width: 24px;
    height: 24px;
    stroke-width: 1.5;
}

.ds-pager .ds-btn:disabled { opacity: var(--ds-opacity-disabled); }
