// ── Local measures ───────────────────────────────────────────────────────────
// Every measure this package can state in a procode-vs-theme token is stated in
// one. What is left is geometry no ramp covers — how wide one particular popup
// is, how much of one particular list should be visible at rest — and it is
// declared here rather than inline, so a value used twice cannot drift apart
// and "is this on the ramp?" is answerable from one block instead of a grep.
//
// Nothing below is a candidate for the theme: a 650px filter panel is this
// toolbar's geometry, not a design-system step.

// How close a modal may come to the viewport edge — one pair for all five
// windows in this package. They ran three different pairs: 85vh/90vw on the two
// GridToolBar dialogs, 85vh/92vw on the column chooser, 90vh/92vw on the two
// alerts. Nothing distinguished them; they were each capped by whoever wrote
// them. Declared with `!default` in all three partials that need it — this
// file and the two alert partials — so each compiles standalone and no copy can
// disagree with the others.
$dialog-viewport-max-width: 92vw !default;
$dialog-viewport-max-height: 90vh !default;

// Anchored popups — each hangs off its own toolbar button and is as wide as its
// contents need: a panel of filter rows, a picker, a menu.
$advanced-popup-width: 650px;
$export-popup-width: 320px;
$quick-filter-popup-width: 225px;
$setting-popup-width: 200px;

// The saved-view control in the toolbar row. It happens to equal the quick
// filter popup's width; named apart so moving one does not silently move the
// other.
$screen-view-width: 225px;

// Modal dialogs.
$chooser-dialog-width: 900px;
// Create New View and Select a new group are the same object — a title, one
// field, one primary action — and they ran 480px and 550px, a difference
// neither could state a reason for. The wider one drew a 550px window around a
// control that took 40% of it. One measure now; the two names stay, because
// each dialog still says which measure it is asking for.
$field-dialog-width: 480px;
$group-filter-dialog-width: $field-dialog-width;
$view-creator-dialog-width: $field-dialog-width;

// Scroll caps. Each bounds one list so it scrolls inside its own surface rather
// than running down the screen; the number is how much of that list should be
// visible at rest, which no ramp can say.
$filter-rows-max-height: 313px;
$export-history-max-height: 200px;
$chooser-pane-height: 400px;
$view-list-max-height: 180px;
$view-copy-list-max-height: 210px;

// The Between editor's two inputs share a row with the operator and field
// pickers; this keeps the pair from growing past its share of the grid cell.
$ranged-input-max-width: 230px;

// The field inside a $field-dialog-width dialog — the copy-source picker in
// Create New View, the group picker in Select a new group. Both hold a short
// value from a short list (a view name is capped at 25 characters), so the 452px
// content column is roughly twice what the widest one can use, and a control
// that wide reads as a field expecting a sentence. A cap, not a width: the field
// is still 100% beneath it, so a dialog narrowed by $dialog-viewport-max-width
// shrinks it too.
$dialog-field-max-width: 260px;

// The gap between a toolbar button and the popup anchored under it. Every
// anchored popup offsets from its own trigger's height (`calc(100% + …)`)
// rather than from a literal, so the four follow the control ramp together.
// They were all `top: 42px` — correct when these buttons were 32px, arbitrary
// once 2.7.0 put them on $control-height, and arbitrary again now that a button
// measures its own content.
$popup-anchor-gap: $space-xs;

// How far an anchored popup travels as it fades in, and back as it fades out.
// One measure for both keyframes, so the two cannot be tuned apart.
$popup-slide-distance: $space-xl;

// The one sub-grid measure in this file. The 4px ladder has no step this small
// and every use is optical rather than structural: an icon's breathing room
// inside its own square, how far the unsaved dot overhangs the control it
// marks, and the vertical inset of a chip whose height is its line box.
$optical-inset: 2px;

// Type tracking. Not spacing — it is part of how a size renders, and the ladder
// has nothing at this scale. One value per job, where there used to be 0.5px on
// one uppercase micro-label and 0.4px on the other.
//
// `$tracking-title` (-0.2px) is gone with the dialog titles' hand-set type: it
// was tightening an 18px line, and the heading classes it gave way to carry no
// tracking of their own at the sizes they render. Nothing here needs a second
// one.
$tracking-caps: 0.4px;

// ── The button box ───────────────────────────────────────────────────────────
// No button in this package states a height. Each measures its own content —
// one line box, plus an equal inset above and below it, plus the hairline the
// theme's `.qo-btn` draws — which is what "line-height + padding-top +
// padding-bottom" means, and what stops a button and the text inside it from
// being tuned apart.
//
// `$btn-line` is a LENGTH rather than a ratio on purpose. It is the single box
// a button centres its content in, and two kinds of content have to land in it:
// one line of $font-size-sm text (12px, which wants ~15.2px) and one $size-sm
// glyph (16px). Left as a ratio the two round apart, and an icon button sits a
// pixel off the text button beside it.
$btn-line: $size-sm !default;       // 16px
$btn-inset-y: $space-xs !default;   // 4px
$btn-inset-x: $space-md !default;   // 12px

// One export format chip: a <button> on this same box — one $btn-line line of
// $font-size-xs, $btn-inset-y above and below, and the hairline — so it
// measures what every other button here measures, 26px. Derived rather than
// measured: it was a 28px literal, which stopped matching the chip the moment
// it became a <button> and took the reset's inherited line box (24px). The
// export scope list's cap is spelled out of this plus the measures around it,
// so the list still shows three whole scopes when any of them moves.
$export-chip-height: $btn-line + (2 * $btn-inset-y) + (2 * $border-width-sm);

// The box above works out to 26px, and that is a floor rather than a taste:
// WCAG 2.5.8 puts the smallest safe pointer target at 24px and the border is
// not part of that target, so one step down the space ladder ($space-1, a 2px
// inset) would land at 22px and break it.
//
// ── Fields ───────────────────────────────────────────────────────────────────
// The value-setting controls — text inputs, dropdowns, multiselects, the global
// search field, the saved-view picker, the advanced-search editors — take a
// height, because Kendo gives them no intrinsic sizing this package can reach.
//
// Deliberately NOT the button box. A button is a target you hit once; a field
// is one you aim at, put a caret in and then read back, and it carries a
// placeholder, sometimes a clear affordance and sometimes wrapping chips. At
// the button's 26px its text sits 4px off each edge and a row of them reads as
// a strip rather than a form. 32px is the theme's own compact control step, so
// this is a name for an existing rung, not a new measure.
$field-height: $control-height-sm !default;   // 32px

// The toolbar row is the one exception to the intrinsic box above, and it is
// one on purpose. Its eight controls — global search, Advanced Search, Quick
// Filter, Group By, Reset, Settings, Export, the saved-view picker — are a
// single band and have to agree to the pixel, and a derived height cannot
// deliver that: the arithmetic starts from a font's line box, and measured on
// the real row it produced 31.6px for five of them and 33.6px for two more
// against the search field's declared 32.
//
// So that row states its height, once, at `.tmpl-grid-toolbar-container > *`
// and on `.tmpl-button-field`. There is deliberately no derived inset token
// here any more — one existed, solving for the inset that would land on
// $field-height, and it is exactly the kind of arithmetic that looks right and
// measures wrong.

// ── Focus ────────────────────────────────────────────────────────────────────
// Kendo halos every button on `:focus` and `.k-focus` — a 2px grey ring that
// appears on a mouse click as well as on a keyboard landing, which is the
// treatment the theme deliberately strips: `.qo-btn` rings on `:focus-visible`
// only. Every one of Kendo's button focus rules is (0,2,0) and this package
// loads after Kendo, so `box-shadow: none` at the same specificity is enough
// and needs no `!important`.
//
// It used to carry one, and that `!important` outranked the theme's own
// `.qo-btn:focus-visible` ring as well — so no button in this package showed
// keyboard focus at all. The ring is restated here at (0,3,0): the one
// treatment every `qo-` control gets, on the buttons that carry `qo-btn`.
//
// The ring DOES carry `!important`, and that is Kendo's doing, not a
// specificity hammer: `.k-button-clear { box-shadow: none !important }` is
// what Kendo emits for every `fillMode="clear"` button — the dialog close
// icons, the row actions, the criteria tools, the filter row's add and remove —
// and only another `!important` reaches past it (doc 09 §6, §9). Measured in
// the harness before this: a focused close icon matched `:focus-visible` and
// drew nothing.
@mixin tmpl-button-focus {
  box-shadow: none;

  &:focus-visible {
    outline: 0;
    box-shadow: $focus-ring !important;
  }
}

.tmpl-popup-animation {
  animation: fadeInWithNavigation 0.3s ease-in-out;
}

.tmpl-popup-animation-2 {
  animation: fadeIn 0.3s ease-in-out;
}

.tmpl-popup-animation-reverse {
  animation: fadeOutWithNavigation 0.25s ease-in-out !important;
}

.tmpl-popup-animation-2-reverse {
  animation: fadeOut 0.3s ease-in-out;
}

.tmpl-error-message {
  color: $text-danger;
  font-size: $font-size-sm;
  line-height: $line-height-sm;
  font-weight: $font-weight-regular;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes fadeInWithNavigation {
  from {
    opacity: 0;
    transform: translateY(-$popup-slide-distance);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

@keyframes fadeOutWithNavigation {
  from {
    opacity: 1;
    transform: translateY(0);
  }

  to {
    opacity: 0;
    transform: translateY(-$popup-slide-distance);
  }
}

.tmpl-grid-toolbar-container {
  display: flex;
  flex-wrap: nowrap;
  // `flex-end`, not `space-between`: the global search is the only growing item,
  // so while it is visible it eats the free space and packing is irrelevant -
  // search on the left, tools on the right. When it is hidden there is no
  // growing item left, and `space-between` used to fan the remaining tools
  // across the whole row. `flex-end` keeps them grouped on the right either way.
  justify-content: flex-end;
  // `center`, not the default `stretch`. Stretch made the row size its items,
  // passing the row height down as `height: 100%` on whatever was inside each
  // slot. Every control here is $field-height by its own declaration now, so
  // there is nothing to stretch — and centring is the honest alignment if one
  // of them ever does differ.
  align-items: center;
  width: 100%;
  // $space-xs, not $space-sm: each tool slot is already a square with its own
  // hover surface, so the wider step read as a loose row against the dense grid
  // below it.
  gap: $space-xs;
  font-family: $font-family-primary;

  // ── One height for the row, stated once ────────────────────────────────────
  // Every tool is a component that renders its own wrapper — a container div, a
  // `rootStyle` root, or both — and the height has to survive that trip. Stated
  // on each tool's own class it did not: Group By and the saved-view picker
  // measured 33.6px against everything else's 31.6, because what the eye sees
  // as "the control" is a wrapper for those two and the button itself for the
  // rest, and only one of the two was being sized.
  //
  // The child combinator is the point. These children ARE the eight tools, one
  // each, so this says "every control in this row is one height" in the one
  // place that can see all eight — rather than eight rules that each have to
  // guess how deep their own wrapper goes. Anything an app adds to the row is
  // caught by it too, which is the right default for a band of controls.
  //
  // Height only — deliberately not `display` or `align-items`. Each tool's
  // wrapper is a Kendo input root, a `position: relative` popup anchor or a
  // plain div, and changing the display type of all eight to fix the height of
  // two is a far larger change than the one being made.
  > * {
    height: $field-height;
  }

  // Must NOT be paired with an auto margin: auto margins absorb free space
  // before flex-grow does, which would collapse the input to its 0 flex-basis.
  .tmpl-global-search {
    flex: 1;
    // The one control in this row that never stated its own height: it used to
    // take it from the row, which stretched every item to the tallest. The row
    // centres its items now (each arrives at $field-height under its own
    // arithmetic), so a control that says nothing falls back to Kendo's rem
    // -derived height and is the only thing in the band at the wrong size.
    height: $field-height;

    // The search and clear affordances are Kendo own `.k-clear-value` spans
    // with no styling from this package, so they arrived at Kendo rem-derived
    // sizes - a 29.6px button inside a 37.6px span - which is square but sits
    // on no ladder this stack owns, and left the magnifier and the cross a
    // different size from every other icon button in the toolbar.
    //
    // Squared on the control ramp instead, at the FULL control height so the
    // affordance owns its own gutter.
    //
    // The box has to be the full height for the glyph to read as centred. At
    // 32px in a 40px field the glyph ended up 18.8px from the field border but
    // only 6px from the separator, because the field padding sat on one side of
    // the box and the separator hugged the other - centred in its box, visibly
    // off-centre in the space. A 40px box with the field padding removed puts an
    // equal 10px on both sides of the 20px glyph.
    padding: 0;

    // The row's type. Every other control in this band renders at
    // $font-size-sm — `tmpl-button` and the saved-view picker — and the
    // search field was made to match with an inline `fontSize: "12px"` in
    // the TSX, beside a `qo-font-md` class saying 13px that Kendo's `.k-input`
    // out-ordered. Stated once here at (0,2,0), which beats `.k-input-md`, and
    // the inner `<input>` takes it through Kendo's own `font: inherit`.
    font-size: $font-size-sm;

    .k-clear-value {
      width: $field-height;
      height: $field-height;
      padding: 0;
      flex: 0 0 $field-height;
      display: inline-flex;
      align-items: center;
      justify-content: center;

      .k-button {
        width: 100%;
        height: 100%;
        min-height: 0;
        padding: 0;
      }
    }

    // The hairline between an affordance and the text. Full-height boxes either
    // side, so it needs no inset of its own beyond the vertical one.
    .k-input-separator {
      margin: $space-1 0;
    }

    // The field padding moved to the affordance boxes, so the text needs its
    // own inset back.
    input {
      padding-left: $space-md;
      padding-right: $space-md;
    }
  }

  .tmpl-advanced-search-container {
    position: relative;
  }

  // The saved-view picker's visible edge. The picker itself is `fillMode="flat"`
  // and draws no box, so this border IS the control — which is why what fills it
  // has to stop at its content edge rather than restate the row height.
  //
  // This is the only tool in the row that draws a border on its OUTER element,
  // and `.tmpl-grid-toolbar-container > *` sizes that element: `box-sizing:
  // border-box` puts the two hairlines inside the 32px, leaving a 30px content
  // box. A 32px picker inside it overhangs by exactly 2px and paints over the
  // bottom border — which reads as the border having gone missing, not as an
  // overflow.
  .tmpl-screen-view-container {
    background-color: $background-default;
    border: $border-width-sm solid $border-default;
    border-radius: $border-radius-md;
  }
  .tmpl-screen-view-wrapper {
    position: relative;
    display: inline-block;
    // Fills the container's content box rather than matching the row. See above.
    height: 100%;
    .tmpl-unsaved-indicator {
      position: absolute;
      // Overhangs the control it marks by the same measure on both axes.
      top: -$optical-inset;
      right: -$optical-inset;
      width: $size-xs;
      height: $size-xs;
      border-radius: $border-radius-full;
      background-color: $background-danger;
      z-index: 1000;
    }
  }
  // ── Elevation ────────────────────────────────────────────────────────────
  // Every shadow in this package comes off the theme's ramp. Four levels, one
  // job each:
  //
  //   $shadow-sm   the active option of a segmented control
  //   $shadow-md   a surface that lifts over content scrolling under it —
  //                the sticky filter header
  //   $shadow-lg   anchored popups: advanced search, export, quick filter,
  //                setting. They hang off a toolbar button and sit on the page.
  //   $shadow-xl   modals: column chooser, Create New View, Select a new group,
  //                and both alert dialogs. They sit over a scrim.
  //
  // Before this there were four hand-rolled values for the top two jobs alone:
  // `-10px 0 50px rgba(0,0,0,.15)` on two of the popups (a leftward blur on a
  // panel that opens below and to the right), `0 0 8px 0 $boxshadow-primary` on
  // the other two — a COLOUR token standing in for a whole shadow — and two
  // different two-layer stacks on the modals. Siblings that open from the same
  // toolbar read as different surfaces.
  .tmpl-advanced-popup {
    @extend .tmpl-popup-animation;
    position: absolute;
    right: 0;
    top: calc(100% + #{$popup-anchor-gap});
    width: $advanced-popup-width;
    z-index: 3;
    background-color: $background-default;
    border: $border-width-sm solid $border-default;
    border-radius: $border-radius-md;
    box-shadow: $shadow-lg;

    .tmpl-filter-container {
      max-height: $filter-rows-max-height;
      overflow-y: auto;
      padding: 0 $space-lg $space-lg;

      .tmpl-filter {
        display: grid;
        // The last two tracks hold the add and remove icons. They were 25px —
        // a literal on no ladder, holding a $size-sm button — so each track
        // spent 9px on nothing and the three editor cells beside them gave up
        // 10px of the row between them. $size-md is the measure every other
        // inline icon in this popup is squared on.
        grid-template-columns: 1fr 1fr 1fr $size-md $size-md;
        gap: $space-sm;
        align-items: center;
        color: $text-default;
        font-size: $font-size-sm;
        line-height: $line-height-sm;
        font-weight: $font-weight-regular;

        &:first-of-type {
          font-weight: $font-weight-semibold;
          font-size: $font-size-xs;
          line-height: $line-height-xs;
          letter-spacing: $tracking-caps;
          text-transform: uppercase;
          color: $text-secondary;
          padding: $space-md $space-lg;
          margin: 0 (-$space-lg);
          width: calc(100% + (2 * $space-lg));
          margin-bottom: $space-lg;
          position: sticky;
          top: 0;
          background-color: $background-default;
          box-shadow: $shadow-md;
          z-index: 1001;
        }

        &.tmpl-filter-error {
          // The theme's danger tint, not an alpha mixed by hand from the solid:
          // the soft family exists so a pale status fill is one token.
          background-color: $background-danger-soft;
          border-radius: $border-radius-md;
          padding: $space-sm $space-md;

          .tmpl-dropdown {
            border-color: $border-danger !important;
          }
        }

        // Why the row above cannot be applied. Spans every track so it reads as
        // a caption under the three cells rather than as a sixth column, and
        // sits inside the tinted block so the tint and the sentence are one
        // thing. The tint alone was the whole of the feedback until 2.9.0.
        .tmpl-filter-error-message {
          grid-column: 1 / -1;
          font-size: $font-size-xs;
          line-height: $line-height-xs;
          font-weight: $font-weight-semibold;
          color: $text-danger;
        }

        &.tmpl-filter-border {
          position: relative;

          & > .tmpl-filter-id {
            position: absolute;
            transform: translate(-50%, -50%);
            top: 0;
            left: 0;
            z-index: 10;
            // Vertical is the line box plus its optical inset; horizontal is
            // the ladder's first step. It was 2px on both, which left the
            // label touching the chip's side borders.
            padding: $optical-inset $space-xs;
            font-size: $font-size-xxs;
            line-height: $line-height-xxs;
            font-weight: $font-weight-bold;
            color: $text-dark;
            background-color: $background-secondary;
            border: $border-width-sm solid $border-default;
            border-radius: $border-radius-md;
          }
        }

        // Value cell for Is Empty / Is Not Empty. There is nothing to collect,
        // but the cell has to keep its share of the five-column grid — dropping
        // it slides the delete buttons under the Value heading.
        .tmpl-advanced-no-value {
          display: block;
          min-height: $field-height;
        }

        .tmpl-advanced-input {
          // The editor takes $field-height whatever widget the filter's type
          // resolved to — TEXT and NUMBER to a TEXTBOX, DATE to a DatePicker,
          // LIST to a picker.
          //
          // This class lands on the widget's OUTER element, which for every one
          // of them is the Kendo wrapper (`.k-input` / `.k-picker` /
          // `.k-datepicker`) holding a borderless `<input>`. **A Kendo field
          // draws its box on that wrapper, not on the input**, so sizing the
          // input alone — which is all this rule did until 2.9.0 — left the
          // date and number cells at Kendo's own height while the text and
          // dropdown cells beside them in the same row sat at $field-height.
          //
          // Two editors are excluded because they have to grow: the radio group
          // wraps its options onto a second line and the multiselect wraps its
          // chips. Both take the measure as a floor instead, below.
          &:not(.tmpl-advanced-radio-group):not(.tmpl-advanced-multiselect) {
            height: $field-height;
          }

          // The one editor in this cell that GROWS: past three or four values a
          // multiselect's chips wrap to a second line, and the flat height above
          // clips them. A floor instead, so it starts level with the field and
          // operator cells beside it and grows downward from there — the same
          // treatment the group-by dialog's multiselect already had.
          //
          // It needs a class of its own because the exclusion cannot be written
          // as `:not(.k-multiselect)`: these classes arrive through `rootStyle`,
          // so they sit on the widget's WRAPPER, and the Kendo element that
          // carries `.k-multiselect` is inside it.
          &.tmpl-advanced-multiselect {
            height: auto;
            min-height: $field-height;
          }

          // The inner input fills the box rather than restating it. Radios and
          // checkboxes are excluded: Kendo draws `.k-radio` as a 16px square,
          // and forcing a height stretches the circle into an oval while
          // leaving its width alone.
          input:not([type="radio"]):not([type="checkbox"]) {
            height: 100%;
          }

          .k-radio-label {
            margin-left: $space-xs;
          }
        }

        // BOOLEAN value cell. The RADIOGROUP widget emits one bare <div> per
        // option and no root wrapper of its own, so every rule below hangs off
        // this container: it is what puts the options on one line, aligned with
        // the field and operator dropdowns beside them.
        .tmpl-advanced-radio-group {
          display: flex;
          align-items: center;
          // Options wrap as whole items when the cell is too narrow for them,
          // which grows the grid row rather than clipping an option away.
          flex-wrap: wrap;
          column-gap: $space-md;
          row-gap: $space-xs;
          // Matches the height of the dropdowns in the two cells to its left,
          // so a boolean row is not visibly shorter than every other row.
          min-height: $field-height;

          // One per option, emitted by the RADIOGROUP widget.
          > div {
            display: flex;
            align-items: center;
          }

          // Kendo renders `<span.k-radio-wrap><input.k-radio></span><label
          // .k-radio-label>` and already sizes and spaces those. Only the label
          // needs anything from us: it is the one piece of text in the row that
          // can run long, and it must not break mid-word.
          .k-radio-label {
            white-space: nowrap;
            font-size: $font-size-sm;
            line-height: $line-height-sm;
            // Matches the value text inside the dropdowns beside it (Kendo
            // renders those at #3d3d3d). The row's inherited $text-default is a
            // full shade lighter and left the labels looking washed out next
            // to the two cells they sit in line with.
            color: $text-dark;
          }

          // The whole cell is disabled until an operator is chosen. The theme's
          // global `*:disabled` rule greys the control but not its label, and
          // the label is a sibling of Kendo's wrap span rather than of the
          // input, so no CSS-only sibling selector reaches it — the renderer
          // sets this class instead. Fading the group keeps one disabled cue
          // for control and label instead of inventing a second grey.
          &.tmpl-advanced-radio-group-disabled {
            opacity: 0.6;

            .k-radio-label {
              cursor: not-allowed;
            }
          }
        }

        .tmpl-advanced-ranged-input {
          max-width: $ranged-input-max-width;
          height: $field-height;
          // The Between editor: two inputs on one line, sharing the cell
          // equally. Layout lives here rather than inline on the wrapper and
          // on its two widget roots, so it is one rule a consumer can reach.
          display: flex;
          align-items: center;
          gap: $space-xs;

          > * {
            flex: 1 1 0;
            min-width: 0;
          }
        }
      }

      .tmpl-criteria {
        display: inline-block;
        text-align: center;
        margin: $space-xl 0;
        /*   border: $border-width-sm solid $border-default;
        border-radius: $border-radius-sm; */
        font-size: $font-size-sm;
        line-height: $line-height-sm;
        font-weight: $font-weight-bold;
        color: $text-dark;
        // $size-xl, not a 45px literal three pixels off it.
        min-width: $size-xl;
        cursor: pointer;
        position: relative;

        &:last-child {
          display: none;
        }

        &::before {
          content: "";
          position: absolute;
          // The stub reaches exactly its own height above the chip, and is
          // drawn with the hairline every other rule in this package uses. It
          // was a -20px literal beside the $size-md it mirrors, at 1.5px —
          // a width that lands on no device pixel.
          top: -$size-md;
          left: 50%;
          height: $size-md;
          width: $border-width-sm;
          transform: translateX(-50%);
          background-color: $border-default;
        }

        &::after {
          content: "";
          position: absolute;
          bottom: -$size-md;
          height: $size-md;
          width: $border-width-sm;
          left: 50%;
          transform: translateX(-50%);
          background-color: $border-default;
        }
      }
    }

    // ── The criteria expression ──────────────────────────────────────────────
    // Its own band between the rows and the footer. It used to be a third item
    // inside `.tmpl-action-container`: an unlabelled ~150px box with two 20px
    // icons crowded into it, sharing a row with Clear Filter, Cancel and Apply.
    // The one control in the panel that takes free text was the smallest thing
    // in it and the only one that never said what it wanted.
    //
    // Divided from the rows above by the same hairline the footer uses below,
    // so the panel reads as three bands — rows, expression, actions.
    .tmpl-criteria-editor {
      padding: $space-md $space-lg;
      border-top: $border-width-sm solid $border-light;

      .tmpl-criteria-editor-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: $space-xs;
      }

      // Matches the FILTER / OPERATOR / VALUE captions over the rows — this is
      // the same kind of label for the same kind of control.
      .tmpl-criteria-label {
        font-size: $font-size-xs;
        line-height: $line-height-xs;
        font-weight: $font-weight-semibold;
        letter-spacing: $tracking-caps;
        text-transform: uppercase;
        color: $text-secondary;
      }

      .tmpl-criteria-editor-tools {
        display: flex;
        align-items: center;
        gap: $space-xs;
      }

      .tmpl-criteria-input {
        width: 100%;
        height: $field-height;
        padding: 0 $space-md;
        border: $border-width-sm solid $border-default;
        border-radius: $border-radius-md;
        background-color: $background-default;
        // The one place in the package that shows an expression rather than
        // prose. Monospaced so `(1) AND (11)` lines up its brackets and a
        // missing one is visible rather than merely absent.
        font-family: $font-family-secondary;
        font-size: $font-size-sm;
        line-height: $line-height-sm;
        font-weight: $font-weight-semibold;
        color: $text-dark;

        &:focus {
          outline: none;
          border-color: $border-primary;
          box-shadow: $focus-ring;
        }

        &.tmpl-criteria-input-error {
          border-color: $border-danger;
        }

        // The placeholder is the expression the rows would produce on their
        // own, so an emptied box shows what Reset would put back rather than
        // nothing at all.
        &::placeholder {
          color: $text-secondary;
          font-weight: $font-weight-regular;
        }
      }

      // Syntax at rest, the reason for a rejection in its place. One line in
      // one position, so the panel does not reflow when an expression goes
      // wrong — the text changes colour, not the layout.
      .tmpl-criteria-hint {
        margin: $space-xs 0 0;
        min-height: calc(#{$font-size-xs} * #{$line-height-xs});
        font-size: $font-size-xs;
        line-height: $line-height-xs;
        color: $text-secondary;

        &.tmpl-criteria-hint-error {
          color: $text-danger;
          font-weight: $font-weight-semibold;
        }
      }
    }

    // Advanced search's footer. Sized by its contents like every other footer
    // in the package — a button that measures itself (`$btn-line`) inside the
    // dialog footer's own `$space-md` band — rather than by a 65px literal that
    // matched neither the ladder nor the 64px it worked out to.
    //
    // Divided by the same hairline the saved-views and Create New View footers
    // use. It was the only one dividing itself with a shadow, and with a colour
    // token (`$boxshadow-primary`) standing in for a whole shadow value.
    .tmpl-action-container {
      display: flex;
      gap: $space-sm;
      align-items: center;
      justify-content: space-between;
      padding: $space-md $space-lg;
      border-top: $border-width-sm solid $border-light;

      // No button sizing here. Both actions carry `qo-btn-md tmpl-button`, so
      // the height and the radius already come from the ramp; restating them on
      // `.k-button` also reached the two 20px criteria icons in this bar, which
      // then needed equal specificity and source order to win their own size
      // back.

      div {
        display: flex;
        align-items: center;
        gap: $space-sm;
      }

      // Cancel is a BUTTON carrying the theme's `qo-btn-link-danger` — ink,
      // hover and the focus ring come from there, and `tmpl-button` gives it
      // the box Apply Filter beside it has. It was a bare <span> with
      // `cursor: pointer` and a colour: clickable, but not focusable, not in
      // the tab order, and the one action in this footer a keyboard could not
      // reach. `tmpl-cancel-action` stays on it as a hook and styles nothing.
    }
  }

  // ── Tool slots ───────────────────────────────────────────────────────────
  // The four icon tools — export, setting, reset, quick filter — are square
  // buttons, and the slot IS the button — `flex: 0 0 auto`, so it is exactly
  // whatever the button box measures and cannot fall out of step with it.
  //
  // It has twice. The tools were 32px with 32px slots; 2.7.0 put them on
  // `qo-btn-icon` (40px) and left the slots at 32, so each button overhung its
  // box by exactly the toolbar's gap of the time and the four rendered edge to
  // edge, with the group measuring 32px narrower than it drew. 2.8.0 restated
  // the slots as `$control-height` — correct, but still a second copy of a
  // number the button was reading from the theme. Deriving both from one place
  // is what ends that.
  .tmpl-export-container {
    // The slot is whatever the button inside it measures. The button used to be
    // stretched to the slot (`height: 100%; width: 100%`) and the slot pinned to
    // a control height — the container sizing the control, twice over.
    flex: 0 0 auto;
    position: relative;


    .tmpl-export-popup {
      @extend .tmpl-popup-animation;
      position: absolute;
      right: 0;
      top: calc(100% + #{$popup-anchor-gap});
      width: $export-popup-width;
      z-index: 10;
      background-color: $background-default;
      border: $border-width-sm solid $border-default;
      border-radius: $border-radius-md;
      box-shadow: $shadow-lg;
      padding: $space-md;

      .tmpl-export-scope-container {
        // Three scope sections tall, spelled out of the same measures the
        // sections below it use: label + the gap under it + a chip row + the
        // gap that opens the next section. Change either measure and this
        // follows.
        max-height: calc(
          (
              (#{$font-size-sm} * #{$line-height-sm}) + #{$space-sm} +
                #{$export-chip-height} + #{$space-md}
            ) * 3
        );
        overflow-y: auto;
        // Scrollbar styling now comes from procode-vs-theme's global rail
        // (utilities/_scrollbar.scss), which is this package's own chooser
        // treatment promoted so every scroll container in the stack matches.
        // The three copies that used to live here had already drifted from it
        // (2px thumb radius, no hover, no track).
      }

      .tmpl-export-scope-section {
        // The anchored-popup pair, the same one "Previous Exports" uses below:
        // $space-md opens a scope, $space-sm spaces inside it. It used to be
        // $space-sm/$space-xs — half a step tighter than every other popup on
        // this toolbar, on the one popup that stacks two labelled zones.
        margin-bottom: $space-md;

        // The last scope's gap is the history header's own $space-md, not a
        // second one paid back to back with it.
        &:last-of-type {
          margin-bottom: 0;
        }

        // The scope caption ("Select Export Format") is the theme's `qo-label`,
        // the same class every form caption in the stack carries — it used to
        // restate a type of its own (11px / 600 / $text-secondary). Layout only
        // here: block, and the section's own $space-sm under it rather than the
        // label's default 4px.
        .tmpl-export-scope-header {
          display: block;
          margin: 0 0 $space-sm 0;
        }
      }

      .tmpl-export-options-grid {
        display: flex;
        gap: $space-sm;
        overflow-x: auto;
        // Scrollbar styling now comes from procode-vs-theme's global rail
        // (utilities/_scrollbar.scss), which is this package's own chooser
        // treatment promoted so every scroll container in the stack matches.
        // The three copies that used to live here had already drifted from it
        // (2px thumb radius, no hover, no track).

        .tmpl-export-option-card {
          flex: 0 0 auto;
          font-size: $font-size-xs;
          font-weight: $font-weight-semibold;
          color: $text-dark;
          display: flex;
          align-items: center;
          justify-content: center;
          text-align: center;
          padding: $space-xs $space-sm;
          border: $border-width-sm solid $border-default;
          border-radius: $border-radius-lg;
          cursor: pointer;
          transition:
            background-color 0.15s ease,
            border-color 0.15s ease,
            color 0.15s ease,
            box-shadow 0.15s ease;
          white-space: nowrap;
          // A <button>, so it is in the tab order. The theme's reset gives it
          // `font: inherit`; the line box is the package's own $btn-line, so
          // the chip measures the same 26px as every other button here.
          line-height: $btn-line;
          // Flat, and every colour a token. The chip used to be a bevel —
          // `lighten()` / `darken()` of $background-light at rest, two alphas
          // of $background-primary on hover, an inset highlight over it and a
          // pressed inset under `:active` — five fills the theme has no name
          // for, on the one element in the stack drawn in relief. The segmented
          // control in Create New View is the same kind of choice drawn flat
          // on the same tokens, so the two now agree. The `--selected` state
          // went with it: nothing in the package ever emitted the class.
          background-color: $background-default;

          &:hover {
            border-color: $border-primary;
            background-color: $background-primary-soft;
            color: $text-primary;
          }

          // The theme's one focus treatment.
          &:focus-visible {
            outline: 0;
            box-shadow: $focus-ring;
          }
        }
      }

      .tmpl-export-divider {
        padding-top: $space-sm;
        display: flex;
        justify-content: flex-end;
      }

      // "Previous Exports" starts a new section — it is not another row of
      // the export picker above it. Before this it sat flush against the
      // format chips (measured 0px apart), so the two zones read as one
      // undifferentiated list. A full-bleed rule plus space above it makes the
      // break legible; the negative inline margin cancels the popup's own
      // padding so the rule spans its full width.
      //
      // The measures are the anchored-popup pair ($space-md to open a section,
      // $space-sm inside it) rather than the dialog pair: this is a 320px
      // dropdown, and at the original $space-lg/$space-md the one separator
      // was spending ~41px of a popup that is mostly list.
      //
      // "Previous Exports" is the theme's `qo-label` too — one caption class for
      // both zones of this popup, where the two used to run 11px and 13px of
      // their own. Layout only here.
      .tmpl-export-history-header {
        display: block;
        margin: $space-md (-$space-md) $space-sm;
        padding: $space-sm $space-md 0;
        border-top: $border-width-sm solid $border-light;
      }

      .tmpl-export-history-items-container {
        max-height: $export-history-max-height;
        overflow-y: auto;
        // Full-bleed, the same way the header's rule above it is: the row
        // separators belong to the popup's width, not to the text column. The
        // negative inline margin is carried here rather than on each row
        // because this is the scroll container — a negative margin *inside* it
        // would overflow horizontally (overflow-y: auto computes overflow-x to
        // auto), and the rows would have hung a scrollbar under the list.
        // The container reaches the popup's edges; each row re-adds the inset
        // as its own padding, so only the rules run edge to edge.
        margin: 0 (-$space-md);

        .tmpl-export-history-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          gap: $space-sm;
          // $space-sm on both sides of every separator, the measure this popup
          // already spaces inside a section with. It was 2px + 2px — a literal
          // that matched nothing else in the toolbar and read as a dense block
          // rather than a list of rows. The side measure is the popup's inset,
          // put back on the column the container just gave up.
          padding: $space-sm $space-md;

          // The outer two edges belong to the popup's inset, not to a row: the
          // first row's top gap is the header's $space-sm and the last row's
          // bottom is the popup's own padding.
          &:first-of-type {
            padding-top: 0;
          }

          &:last-of-type {
            padding-bottom: 0;
          }

          // Bottom rather than top, so the last row does not draw one — the
          // popup's own border is the list's bottom edge.
          &:not(:last-of-type) {
            border-bottom: $border-width-sm solid $border-default;
          }

          .tmpl-export-history-item-info {
            p {
              &:first-of-type {
                font-size: $font-size-sm;
                font-weight: $font-weight-regular;
                line-height: $line-height-sm;
                color: $text-dark;
              }

              &:last-of-type {
                font-size: $font-size-xxs;
                font-weight: $font-weight-medium;
                line-height: $line-height-xxs;
                color: $text-default;
              }
            }
          }

          .tmpl-export-history-item-action {
            display: flex;
            align-items: center;
          }
        }
      }
    }
  }

  .tmpl-setting-container {
    // The slot is whatever the button inside it measures. The button used to be
    // stretched to the slot (`height: 100%; width: 100%`) and the slot pinned to
    // a control height — the container sizing the control, twice over.
    flex: 0 0 auto;
    position: relative;


    .tmpl-setting-popup {
      @extend .tmpl-popup-animation;
      position: absolute;
      right: 0;
      top: calc(100% + #{$popup-anchor-gap});
      width: $setting-popup-width;
      z-index: 3;
      background-color: $background-default;
      border: $border-width-sm solid $border-default;
      border-radius: $border-radius-md;
      box-shadow: $shadow-lg;

      // ── The package popup row ──────────────────────────────────────────────
      // This is the row every other list and menu row in the package is matched
      // to, and it is matched to the DROPDOWN / MULTISELECT popups the whole app
      // draws (`procode-kendo-widget`, `Dropdown/Style/index.scss`). The four
      // measures below come straight off the theme, which is how the two
      // packages stay identical without either holding a copy of the other's
      // numbers.
      //
      // It states no height. The row is its line box plus an equal inset above
      // and below, so a label that wraps grows its row rather than being
      // clipped — the same rule the buttons follow.
      //
      // A menu row is full-bleed — its hover band spans the popup — so this
      // padding IS the popup's inset on the horizontal.
      div {
        padding: $space-sm $space-md;
        cursor: pointer;
        transition: background-color 0.25s ease-in-out;
        color: $text-dark;
        font-size: $font-size-md;
        line-height: $line-height-md;
        font-weight: $font-weight-regular;

        &:first-of-type {
          border-bottom: $border-width-sm solid $border-default;
        }

        &:hover {
          background-color: $background-secondary;
        }
      }
    }

    // ── Popup insets ─────────────────────────────────────────────────────────
    // The four *anchored* popups — advanced search (650), export (320), quick
    // filter (225) and setting (200) — hang off their toolbar button rather
    // than centring on screen. Their content inset follows the same rule the
    // dialogs use: it tracks the popup's width, so the content column stays
    // proportionate rather than identical.
    //
    //   advanced search   $space-lg    a 650px panel of filter rows
    //   export            $space-md
    //   quick filter      $space-md    square, and the same measure again on
    //                                  both sides of every row separator — one
    //                                  gap size for the whole popup
    //   setting           $space-sm $space-md   menu rows: full-bleed hover
    //                                          band, so tight vertically
    //
    // Setting is the only split pair, and only because a menu row's height comes
    // from its hover band rather than from its padding. Everywhere else the
    // inset is square: quick filter carries it on the popup itself, so its four
    // sides are one declaration and cannot drift apart.
    //
    // Within a popup, sections open with $space-md and space inside themselves
    // with $space-sm — half the dialogs' $space-lg/$space-md pair, because
    // these are dropdowns and mostly list.
    //
    // Advanced search's $space-lg is load-bearing beyond padding: its sticky
    // header row cancels it with `margin: 0 (-$space-lg)` and re-adds it in
    // `width: calc(100% + (2 * $space-lg))`. Change the container's inset and
    // those two have to move with it.

    // ── Dialog rhythm ────────────────────────────────────────────────────────────
    // Three of this package's popups are full dialogs — the column chooser, Create
    // New View and Select a new group. They open in the same app, often minutes
    // apart, so they share one vertical rhythm, one side measure and one type
    // scale. The side padding used to track each dialog's width ($space-xxxl for
    // the 900px chooser, $space-xl for the 480/550px popups). Proportionate in
    // isolation — and the reason the three read as unrelated windows, with 32px,
    // 20px and 20px gutters on dialogs that open minutes apart. One measure now:
    // $space-lg, the same one the vertical rhythm uses.
    //
    //   header   padding: $space-lg <side> $space-md   title $font-size-xl,
    //                                                  $text-darker
    //   body     padding: $space-lg <side> 0           bottom is the footer's
    //   footer   padding: $space-md <side> $space-lg   full-bleed, border-top
    //   field    margin-bottom: $space-lg
    //
    // The footers are full-bleed the same way `.tmpl-title` is: negative inline
    // margins cancel the body's side padding, matching inline padding puts the
    // content back on the body's column. Their bottom padding belongs to the
    // footer, never to the body — Create New View's footer is `position: sticky`,
    // and sticky offsets from the padding box, so a body padding-bottom leaves the
    // bar floating short of the dialog's bottom edge.

    .tmpl-columnchooser-popup {
      @extend .tmpl-popup-animation-2;
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      margin: auto;
      // Wide enough that a long column label and both toggles fit on one line
      // without the label having to ellipsise.
      width: min($chooser-dialog-width, $dialog-viewport-max-width);
      max-width: $chooser-dialog-width;
      height: fit-content;
      max-height: $dialog-viewport-max-height;
      z-index: 4;
      background-color: $background-default;
      border: none;
      border-radius: $border-radius-lg;
      padding: 0;
      box-shadow: $shadow-xl;
      display: flex;
      flex-direction: column;
      overflow: hidden;

      .tmpl-chooser-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        // Shared dialog rhythm — see "Dialog rhythm" above. Asymmetric on
        // purpose: a little more air above the title than below it, where the
        // extra measure only pushed the divider down the screen.
        padding: $space-lg $space-lg $space-md;
        border-bottom: $border-width-sm solid $border-light;
        flex-shrink: 0;

        // The dialog title states NO type of its own — see "Dialog titles" at
        // the foot of this file. Ink is the one thing left, because the theme's
        // heading classes carry size, leading and weight and no colour, and
        // these popups are portalled out of `.qo-page` so there is no ink to
        // inherit but the document default.
        h3 {
          color: $text-darker;
        }

        & > div {
          display: flex;
          align-items: center;
          gap: $space-sm;
        }
      }

      .tmpl-chooser-body {
        display: flex;
        // Base for the secondary text in here — the "Include in card" and
        // "Freeze" captions, and the colour every label inherits. Both were
        // measured off the reference screenshot: captions ~11px, body ink
        // #595959 rather than the darker #464646 this used to run at.
        font-size: $font-size-sm;
        line-height: $line-height-sm;
        color: $text-secondary;
        // One spacing for every control-to-caption gap in the dialog. Read by
        // `withLabel`'s wrapper (kendo-widget), which defaults to 2px.
        --prc-label-gap: #{$space-sm};
        gap: 0;
        flex: 1;
        min-height: 0;
        overflow: hidden;
        // A floor under both panes. It has to sit here, on the non-scrolling
        // container: the panes' own `padding-bottom` is inside their scroll
        // box, so it only appears once the list is scrolled to the end, and
        // until then the rows ran flush into the dialog's bottom edge.
        padding-bottom: $space-lg;

        & > div {
          height: $chooser-pane-height;
          overflow-y: auto;
          position: relative;

          // Scrollbar styling now comes from procode-vs-theme's global
          // rail (utilities/_scrollbar.scss). These panes ARE the design
          // reference for it, so the rule was promoted to the theme and
          // the copy removed rather than the other way round.

          // ── Available columns ──────────────────────────────────────────────
          &:first-of-type {
            flex: 0 0 36%;
            // Asymmetric on purpose — see the scroll-rail note on the sibling
            // pane below. The rail lives in the right gutter.
            padding: 0 $space-1 $space-lg $space-lg;
            border-right: $border-width-sm solid $border-light;

            display: flex;
            flex-direction: column;

            // The row is `withLabel`'s wrapper. It is NOT `.tmpl-column`:
            // that class is `widgetStyle.className`, which Kendo puts on the
            // `k-checkbox-wrap` — the 16px box itself — so padding it merely
            // shoved the caption away from its checkbox and confined the
            // hover highlight to the box. Same trap as `.tmpl-include-card`
            // and `.tmpl-radio` on the right-hand rows.
            .prc-label-field {
              // The package popup row — see the note on `.tmpl-setting-popup div`.
              // The row is its line box plus this inset, so a long label wraps
              // and grows the row instead of being clipped. It carried a
              // `min-height: $field-height` floor as well, which sat below the
              // box it was floor to and so never once applied.
              padding: $space-sm $space-md;
              border-radius: $border-radius-md;
              transition: background-color 0.15s ease;
              white-space: nowrap;

              &:hover {
                background-color: $background-secondary;
              }
            }
          }

          // ── Selected columns ───────────────────────────────────────────────
          &:last-of-type {
            flex: 1;
            // Zero top padding so `.tmpl-title` can stick flush to the pane
            // edge; the rest keeps the list off the popup's edges.
            //
            // The right gutter is $space-1, not $space-lg, because the scroll
            // rail is laid out at the right edge of the PADDING box: at 14px it
            // sat against the dialog edge with 14px of dead space between it and
            // the rows it scrolls, reading as window chrome rather than as part
            // of the list. At 4px it sits just clear of the content. The rail
            // occupies what the left gutter spends on padding, so the two sides
            // still balance optically.
            padding: 0 $space-1 $space-lg $space-lg;
            background-color: $background-light;

            // One grouped list rather than a stack of cards: hairline dividers
            // between rows read as a single object and scan far faster than
            // repeated borders + shadows + gaps.
            .tmpl-chooser-droppable {
              background-color: $background-default;
              border: $border-width-sm solid $border-light;
              border-radius: $border-radius-md;
              overflow: hidden;
            }

            .tmpl-chooser-setting-container {
              // The package popup row, same as the pane opposite. The
              // `min-height: $field-height` floor it used to carry was below
              // the box and never applied.
              display: flex;
              align-items: center;
              justify-content: space-between;
              gap: $space-md;
              background-color: $background-default;
              padding: $space-sm $space-md;
              border-bottom: $border-width-sm solid $border-light;
              transition: background-color 0.15s ease;

              &:hover {
                background-color: $background-light;
              }

              .tmpl-chooser-setting-label {
                flex: 1;
                min-width: 0;
                display: flex;
                align-items: center;
                gap: $space-sm;

                // The drag handle is the row's affordance — muted until the row
                // is hovered, and always the grab cursor.
                //
                // The grip is painted here rather than left to Kendo's
                // `k-i-drag`: that is a *font* icon, and this stack ships the
                // SVG icon set only (every other icon here renders as
                // `k-svg-icon`), so the glyph resolved to nothing and the span
                // measured 0px wide — the rows were drag-reorderable with no
                // visible handle at all. A radial-gradient dot grid needs no
                // webfont and no extra markup, which matters because the span
                // must stay a plain element: react-beautiful-dnd refuses to
                // start a drag from a <button>.
                .k-i-drag {
                  flex: 0 0 auto;
                  display: block;
                  width: $size-xs;
                  height: $size-sm;
                  color: $border-secondary;
                  // Dot radii are glyph geometry, not spacing — the grid itself
                  // is derived from the size tokens: two columns across
                  // `$size-xs`, three rows down `$size-sm`.
                  background-image: radial-gradient(
                    currentColor 1.4px,
                    transparent 1.5px
                  );
                  background-size: calc($size-xs / 2) calc($size-sm / 3);
                  background-position: center;
                  cursor: grab;
                  transition: color 0.15s ease;

                  &::before {
                    content: none;
                  }

                  &:active {
                    cursor: grabbing;
                  }

                  // Primary rows sit outside a <Draggable> and cannot be
                  // reordered. Hidden rather than removed so their label
                  // still lines up with every other row.
                  &.tmpl-drag-disabled {
                    visibility: hidden;
                    cursor: default;
                  }
                }

                p {
                  font-size: $font-size-md;
                  line-height: $line-height-md;
                  font-weight: $font-weight-regular;
                  color: $text-secondary;
                  overflow: hidden;
                  text-overflow: ellipsis;
                  white-space: nowrap;
                }
              }

              &:hover .tmpl-chooser-setting-label .k-i-drag {
                color: $text-secondary;
              }

              & > div {
                display: flex;
                align-items: center;
                gap: $space-md;
                flex: 0 0 auto;

                // Fixed tracks so the toggles line up down the column instead
                // of drifting with each label's width. These must sit on the
                // widget *roots* (`<section>` / the freeze click-wrapper) —
                // `.tmpl-include-card` is on Kendo's `k-checkbox-wrap` and
                // `.tmpl-radio` on the `<input>`, so sizing those stretches the
                // box itself into a pill.
                & > section:first-of-type {
                  flex: 0 0 auto;
                  min-width: $size-xxl + $size-xl;
                }

                & > div {
                  flex: 0 0 auto;
                  min-width: $size-xxl;
                }

                & > section:last-of-type {
                  flex: 0 0 auto;
                }

                .k-i-close {
                  cursor: pointer;
                }
              }
            }

            // react-beautiful-dnd renders the primary row outside the
            // Draggable wrapper, so `:last-child` alone cannot find the true
            // last row — clear the divider on both shapes.
            .tmpl-chooser-droppable
              > *:last-child
              .tmpl-chooser-setting-container,
            .tmpl-chooser-droppable
              > .tmpl-chooser-setting-container:last-child {
              border-bottom: none;
            }
          }

          .tmpl-search-container {
            // Top padding matches `.tmpl-title`'s across the divider, which
            // lands the top of the search box exactly level with the top of
            // the count text — the two panes start on the same line. Change
            // one and change the other.
            // Bottom padding belongs to the gap below, not to the box: 12px
            // to the first row, matching what `.tmpl-title` leaves across the
            // divider so both lists start on the same line AND keep the same
            // gap under their headers.
            padding: $space-md 0 0;
            margin-bottom: $space-md;
            position: sticky;
            top: 0;
            background-color: $background-default;
            width: 100%;
            z-index: 1;

            .tmpl-search {
              height: $field-height;
              border-radius: $border-radius-md;
            }

            // Kendo's font icon carries its own box; centre it in the adornment
            // slot and mute it so it reads as a hint, not a control.
            .tmpl-search-icon {
              display: flex;
              align-items: center;
              color: $text-placeholder;
              margin-inline-start: $space-sm;
            }
          }

          .tmpl-title {
            // Full-bleed band across the pane, so the count reads as a section
            // header rather than floating text. The negative inline margin
            // cancels the pane's side padding.
            //
            // Vertical is the band's height and is kept tight — the count is a
            // one-line label, not a masthead. Horizontal must stay whatever
            // the pane own side padding is ($space-lg): it is the same measure
            // the negative margin cancels, and it is what lines the count up
            // with the rows below it. Move one and the other two move with it.
            padding: $space-md $space-lg;
            margin: 0 (-$space-1) $space-md (-$space-lg);
            border-bottom: $border-width-sm solid $border-light;
            position: sticky;
            top: 0;
            background-color: $background-light;
            width: auto;
            z-index: 1;

            p {
              // A count, not a heading: one step down from the dialog title's
              // scale so it labels the pane instead of competing with "Column
              // Chooser" above it.
              font-size: $font-size-md;
              line-height: $line-height-md;
              font-weight: $font-weight-semibold;
              color: $text-secondary;
              letter-spacing: 0;
            }
          }
        }
      }

      .tmpl-no-result {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: $space-lg;
        color: $text-default;
        font-size: $font-size-md;
      }
    }
  }

  .tmpl-screen-view {
    width: $screen-view-width;
    // `100%`, NOT $field-height. The row height belongs to
    // `.tmpl-screen-view-container`, which draws this control's border and
    // therefore keeps two hairlines of that height for itself; a picker that
    // restated 32px here overhung the container's 30px content box and painted
    // over its bottom border.
    height: 100%;

    & > button {
      border: none;
      color: $text-secondary;
      font-size: $font-size-sm;
      font-weight: $font-weight-regular;
      line-height: $line-height-sm;

      // The caret is a square on the control ramp, and the label takes what
      // is left. Both were 40px literals — right by coincidence, and with
      // nothing tying them to the height beside them.
      &:first-of-type {
        width: calc(100% - #{$field-height});
        height: 100%;
      }

      &:last-of-type {
        width: $field-height;
        height: 100%;
      }
    }
  }

  .tmpl-reset {
    // The slot is whatever the button inside it measures. The button used to be
    // stretched to the slot (`height: 100%; width: 100%`) and the slot pinned to
    // a control height — the container sizing the control, twice over.
    flex: 0 0 auto;
  }

  .tmpl-quick-filter-container {
    // The slot is whatever the button inside it measures. The button used to be
    // stretched to the slot (`height: 100%; width: 100%`) and the slot pinned to
    // a control height — the container sizing the control, twice over.
    flex: 0 0 auto;
    position: relative;


    .tmpl-quick-filter-popup {
      @extend .tmpl-popup-animation;
      position: absolute;
      right: 0;
      top: calc(100% + #{$popup-anchor-gap});
      width: $quick-filter-popup-width;
      z-index: 3;
      background-color: $background-default;
      border: $border-width-sm solid $border-default;
      border-radius: $border-radius-md;
      box-shadow: $shadow-lg;
      // ONE measure, $space-md, for every gap in this popup: its four sides, and
      // both sides of every separator. Nothing here is spaced by anything else.
      //
      // The inset belongs to the popup, not to the rows. The rows used to carry
      // it, which is what made the four sides disagree: the horizontal inset was
      // paid once (by the row) but the vertical was paid twice at the foot (row
      // + the wrapper inside it), and the first row's top and the last row's
      // bottom were the only two edges anyone could tune. Owning it here means
      // top, left, right and bottom are one declaration and cannot drift apart.
      padding: $space-md;

      // `>`, not a descendant selector, and it is load-bearing. `Filter.tsx`
      // passes `tmpl-dropdown-container` **twice** — once on the row `<div>` it
      // writes itself, and again as the dropdown's `rootStyle`, which the
      // standard widget renders as a `<section>` *inside* that row. A
      // descendant selector therefore hit both, and the inner copy took the
      // inset a second time: the dropdown (`width: 100%` of its wrapper) sat one
      // inset right of the label it belongs to and one inset short of the
      // right edge. The child combinator scopes everything below to the row.
      > .tmpl-dropdown-container {
        // The row's only job now is the gap around the separator, and it is the
        // popup's own inset again — each row pays $space-md on both edges, so a
        // divider sits centred in the same measure that frames the popup.
        padding: $space-md 0;

        // The outer two edges are the popup's, not a row's — otherwise the
        // first row's top and the last row's bottom would each pay the inset
        // twice and the four sides would stop matching.
        &:first-of-type {
          padding-top: 0;
        }

        &:last-of-type {
          padding-bottom: 0;
        }

        // The row's own label, and ONLY it. `> span` is as load-bearing as the
        // combinator above: Kendo builds a dropdown out of nested spans — the
        // picker itself, `.k-input-inner`, the caret's icon — and each of them
        // is the first span among its siblings, so a bare `span:first-of-type`
        // reaches inside the widget and restyles its internals. Type and colour
        // leaking in there was survivable; `display` and the trim below are not
        // (they flattened the picker's flex row and pulled the placeholder and
        // the caret off centre).
        //
        // The label is the theme's `qo-label` (12px / 600 / $text-darker) — the
        // same caption every dialog and the export popup carry — and this rule
        // no longer states type or ink of its own (it ran 13px / 400 /
        // $text-secondary). It DOES state the line box: `qo-label` leaves
        // line-height at `normal`, which is font-dependent and would make the
        // trim below unmeasurable, so the label takes the ramp step paired
        // with its size and the trim derives from that.
        > span:first-of-type {
          line-height: $line-height-sm;
          // Block, so the trim below has something to hang off.
          display: block;

          // The gap between a label and the field it names. It had none: the
          // label sat directly on the picker, so the pair read as one block of
          // text rather than as a caption and a control.
          //
          // $space-xs, the smallest step on the ladder — this is the gap INSIDE
          // a row, and it has to stay clearly smaller than the $space-md that
          // separates one row from the next, or the label starts to look like
          // it belongs to the field above it.
          margin-bottom: $space-xs;

          // Leading trim, both edges. Every gap in this popup is declared as
          // padding or margin, but a line box is taller than its text:
          // $line-height-sm puts half the difference above the glyphs and half
          // below. Untrimmed, that silently added ~3px to every gap that opens
          // or closes on a label — the popup's top inset, the space under each
          // separator, and now the margin above.
          //
          // Pulling back half the leading on each side makes the label's box
          // hug its text, so the measure that declares a gap is the gap.
          // Derived from the line-height token, not measured — change
          // $line-height-sm and both follow.
          &::before,
          &::after {
            content: "";
            display: block;
            height: 0;
          }

          &::before {
            margin-top: calc((1 - #{$line-height-sm}) * 0.5em);
          }

          &::after {
            margin-bottom: calc((1 - #{$line-height-sm}) * 0.5em);
          }
        }

        // Full-bleed: the divider belongs to the popup's width, not to the text
        // column, so the row cancels the popup's side inset with a negative
        // margin and re-adds it as its own padding — the same trick the
        // advanced-search sticky header row uses. Content stays on the column;
        // only the rule reaches the edges. Bottom rather than top, so the last
        // row does not draw one.
        &:not(:last-of-type) {
          border-bottom: $border-width-sm solid $border-default;
          margin: 0 (-$space-md);
          padding-left: $space-md;
          padding-right: $space-md;
        }
      }
    }
  }

  .tmpl-group-filter-container {
    height: 100%;
    position: relative;

    .tmpl-group-filter-popup {
      @extend .tmpl-popup-animation-2;
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      margin: auto;
      width: $group-filter-dialog-width;
      max-width: $dialog-viewport-max-width;
      height: fit-content;
      // Capped and columnar so the body can be the scroll section, matching
      // the column chooser and Create New View.
      max-height: $dialog-viewport-max-height;
      display: flex;
      flex-direction: column;
      z-index: 3;
      background-color: $background-default;
      border: none;
      border-radius: $border-radius-lg;
      padding: 0;
      box-shadow: $shadow-xl;
      overflow: hidden;

      .tmpl-group-filter-popup-header {
        flex: 0 0 auto;
        display: flex;
        justify-content: space-between;
        // Centred on the title, as in Create New View. It was `flex-start`, for
        // a subtitle paragraph this header no longer carries.
        align-items: center;
        // Shared dialog rhythm — see "Dialog rhythm" above.
        padding: $space-lg $space-lg $space-md;
        border-bottom: $border-width-sm solid $border-light;

        // Ink only — see "Dialog titles" at the foot of this file. The `p` rule
        // that sat here styled the subtitle this header lost in 2.8.3 and has
        // matched nothing since.
        h3 {
          color: $text-darker;
        }
      }

      .tmpl-group-filter-popup-body {
        // The scroll section. min-height:0 is what lets a flex child shrink
        // below its content and actually scroll rather than pushing the footer
        // out of the dialog.
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        // No bottom padding: the footer is the bottom edge and carries its own.
        padding: $space-lg $space-lg 0;

        .tmpl-dropdown-container {
          width: 100%;
          margin-bottom: $space-lg;

          .tmpl-dropdown {
            // Capped like the copy-source picker in Create New View — one
            // measure for the field inside either dialog. It was 40% of the
            // container, which is the same idea stated as a ratio: it moved
            // whenever the dialog moved, and on the old 550px dialog it came out
            // at 220px.
            max-width: $dialog-field-max-width;
            // A MULTISELECT is the one control here that grows: past three or
            // four groups its chips wrap to a second line. `.tmpl-dropdown` sets
            // a flat `height`, which clipped them. The floor keeps it level with
            // the 40px controls it sits among at rest.
            height: auto;
            min-height: $field-height;
            border-radius: $border-radius-md;
            transition: border-color 0.15s ease;

            &:hover {
              border-color: $border-secondary;
            }

            &:focus-within {
              border-color: $border-primary;
            }
          }

          // "Group By" — the same caption as "View Name" over the same kind of
          // control: the theme's `qo-label`, which carries the type and ink.
          // Layout only here.
          & > span {
            &:first-of-type {
              display: block;
              margin: 0 0 $space-1;
            }
          }
        }

      }

      // A sibling of the body, not a child of it — otherwise the action button
      // scrolls away with the content it acts on. No negative margin here: as a
      // sibling there is no body padding to cancel, so it is full-bleed already.
      .tmpl-group-filter-popup-footer {
        flex: 0 0 auto;
        padding: $space-md $space-lg $space-lg;
        border-top: $border-width-sm solid $border-light;
        display: flex;
        justify-content: flex-end;
        gap: $space-sm;
      }
    }
  }
}

.tmpl-button-popup {
  border-radius: $border-radius-md;
  margin-top: $space-1;
  max-height: $view-list-max-height;
  // The popup is the rounded box, so it has to be the one that clips. With the
  // scroll living here a selected row - which Kendo paints edge to edge on its
  // own .k-list-item wrapper - squared off the top corners. The list below owns
  // the overflow instead, which also keeps the footer action in view rather
  // than scrolling it away with the rows it acts on.
  overflow: hidden;
  display: flex;
  flex-direction: column;

  .k-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  .k-list-footer {
    flex: 0 0 auto;
  }
}

// The saved-view row's content. It is rendered INSIDE Kendo's `<li>`, which
// `UserViewView`'s `itemRender` gives `prc-dropdown-list-item` — so the row box
// (inset, type, hover band) is already paid there, by procode-kendo-widget.
//
// This rule used to pay it a second time: `padding: $space-1 $space-sm` on top
// of the li's `$space-sm $space-md`, under a flat `height` that then fought
// both. A saved-view row measured ~48px against the ~33px every other list and
// menu row in these popups draws. What is left here is the row's own layout —
// the name taking the free space, the two actions at the end — and nothing
// about its size.
.tmpl-screen-view-item {
  display: flex;
  width: 100%;
  // $space-xs, the same gap `.tmpl-grid-toolbar-container` uses between its own
  // tools. A row's actions are the same thing in miniature — a run of icon
  // buttons, each with its own hover surface — so they are spaced by the same
  // measure rather than by a step the list picked for itself. It was $space-sm,
  // which read as a loose group against a dense list.
  gap: $space-xs;
  align-items: center;
  // Belt and braces with the name's `flex: 1` below: the actions sit at the
  // right edge even on a row where the name is short, empty, or ever stops
  // being the flexing item.
  justify-content: flex-end;
  transition: background-color 0.25s ease-in-out;
  &.tmpl-view-item-heighlight:hover {
    .tmpl-set-default {
      color: $text-light;
    }

    .tmpl-delete-icon {
      color: $text-light;
    }
  }
  &:hover {
    .tmpl-set-default,
    .tmpl-delete-icon,
    .tmpl-rename-icon {
      visibility: visible;
    }
  }

  // The row's actions, as one group — which is what lets the two gaps here be
  // two measures instead of one.
  //
  // Between the group and the name is the row's own `$space-xs`: they are
  // different kinds of thing and want air between them. Between the buttons
  // INSIDE it is `$optical-inset`, tighter than the 4px ladder's smallest step,
  // because these four are one control: square buttons that each carry their
  // own hover surface, reading as a unit rather than as four separate things.
  // This file's note on `$optical-inset` covers why a sub-grid measure is
  // allowed — every use of it is optical rather than structural, and this is
  // the same job as an icon's breathing room inside its own square.
  .tmpl-screen-view-actions {
    display: flex;
    align-items: center;
    gap: $optical-inset;
  }

  // The name takes the free space, which is what pins the actions after it to
  // the row's right edge.
  //
  // `min-width: 0` is the load-bearing line. A flex item defaults to
  // `min-width: auto`, which floors it at its min-content width — and for
  // `white-space: nowrap` that is the whole string. So a long view name refused
  // to shrink, the ellipsis below it never engaged, and the row's actions were
  // pushed past the popup's right edge instead of sitting against it. The
  // ellipsis was there all along; it could not fire without this.
  .tmpl-screen-view-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }


  // The row's actions — rename, set-default, delete. All three carry
  // `tmpl-button-icon`, so the square, the inset and the glyph size come from
  // the one button box and none of them states a measure of its own.
  //
  // Two things here are about the ROW, not the buttons.
  //
  // `visibility`, not `display`. Hidden with `display: none` the actions took no
  // space, so the row was as tall as its text line (~17px) at rest and as tall
  // as a 26px button the moment the pointer entered it — the list grew under
  // the cursor and every row below the one being pointed at moved. Reserved
  // instead, the row measures the same whether it is hovered or not.
  //
  // And the negative block margin folds the 26px target back into the row's own
  // vertical inset, which is dead space. Without it the actions would set the
  // row height — 26 + the row box's 16 = 42px — and the saved-view list would
  // stand a step taller than every other popup row in the stack for the sake of
  // three buttons that are only sometimes visible. The hit area is still the
  // full 26px; it just overlaps padding rather than adding to it.
  .tmpl-rename-icon,
  .tmpl-set-default,
  .tmpl-delete-icon {
    flex: 0 0 auto;
    visibility: hidden;
    margin-block: -$space-xs;
  }
}

.tmpl-screen-view-footer {
  padding: $space-sm;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  // Divides the action from the rows above it - the same hairline every other
  // footer in this package uses.
  border-top: $border-width-sm solid $border-light;
}

.tmpl-screen-creater-popup {
  @extend .tmpl-popup-animation-2;
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  width: $view-creator-dialog-width;
  max-width: $dialog-viewport-max-width;
  height: fit-content;
  // The popup must not grow with its contents: the summary, the error text and
  // the copy selection all vary in size. Cap it and let the body scroll, so the
  // header and the Create button never walk off screen.
  max-height: $dialog-viewport-max-height;
  display: flex;
  flex-direction: column;
  z-index: 3;
  background-color: $background-default;
  border: none;
  border-radius: $border-radius-lg;
  padding: 0;
  box-shadow: $shadow-xl;
  overflow: hidden;

  .tmpl-screen-creater-popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: $space-md;
    flex: 0 0 auto;
    // Shared dialog rhythm — see "Dialog rhythm" above.
    padding: $space-lg $space-lg $space-md;
    border-bottom: $border-width-sm solid $border-light;

    // Ink only — see "Dialog titles" at the foot of this file. The `p` rule
    // that sat here styled the subtitle this header lost and has matched
    // nothing since.
    h3 {
      color: $text-darker;
    }
  }

  .tmpl-screen-creater-popup-body {
    // No bottom padding: the footer is the bottom edge and carries its own.
    padding: $space-lg $space-lg 0;
    // Only this section scrolls; the header and footer stay put.
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;

    .tmpl-dropdown-container {
      width: 100%;
      flex: 0 0 auto;
      margin-bottom: $space-lg;


      .tmpl-input {
        border-radius: $border-radius-md;
        transition: border-color 0.15s ease;

        input {
          height: 100%;
          // The value the user types, at $font-size-xs. Kendo's own
          // `.k-input-inner` is 0.875rem (14px), a step above everything else
          // in this dialog and two above the toolbar's own fields — so the one
          // free-text box in the package looked like it belonged to a
          // different form.
          font-size: $font-size-xs;
        }

        &:hover {
          border-color: $border-secondary;
        }

        &:focus-within {
          border-color: $border-primary;
        }
      }

      // Direct child only: a descendant `span` also matches Kendo own
      // `span.k-textbox` input wrapper, and the margin below then lands on top
      // of the gap between fields.
      //
      // The caption is the theme's `qo-label` — 12px / 600 / $text-darker,
      // with its required marker drawn by `qo-label-required` — the same
      // class every form label in the consuming app carries. This rule used
      // to restate a type of its own (12px / 500 / $text-dark) beside a
      // hand-coloured `*`; what is left is layout. The caption stays a step
      // ABOVE the value it labels — 12px over the 11px input, which is the
      // pairing this dialog is drawn at.
      & > span {
        display: block;
        // Bound to the control below it — see the note on label spacing.
        margin-bottom: $space-1;
      }

      // "Start With" — a segmented control rather than a radio stack: one
      // click to switch, one row of vertical space, and the active option is
      // readable at a glance instead of needing a dot scan.
      .tmpl-view-source {
        display: flex;
        gap: $space-xs;
        padding: $space-xs;
        background-color: $background-light;
        border: $border-width-sm solid $border-light;
        border-radius: $border-radius-md;

        .tmpl-view-source-option {
          flex: 1;
          padding: $space-sm $space-md;
          border: none;
          border-radius: $border-radius-sm;
          background: transparent;
          cursor: pointer;
          color: $text-secondary;
          font-family: inherit;
          font-size: $font-size-md;
          font-weight: $font-weight-medium;
          line-height: $line-height-md;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          transition:
            background-color 0.15s ease,
            color 0.15s ease,
            box-shadow 0.15s ease;

          &:hover:not(.tmpl-view-source-option--active) {
            color: $text-dark;
            background-color: rgba($shadow-color, 0.04);
          }

          // The theme's one focus treatment, not an outline of its own.
          &:focus-visible {
            outline: 0;
            box-shadow: $focus-ring;
          }

          &--active {
            background-color: $background-default;
            color: $text-primary;
            box-shadow: $shadow-sm;
          }
        }
      }

      // The copy source is one of a handful of view names, so it is capped
      // rather than run to the full content column the way the free-text name
      // field is. Marked with its own class: `tmpl-dropdown-container` is the
      // shared row wrapper, and a `:nth-child` here would move the day a field
      // is added above it.
      &.tmpl-view-copy-field .tmpl-dropdown {
        max-width: $dialog-field-max-width;
      }

      // A long view name must ellipsize inside the picker instead of widening
      // the popup — the trigger is a flex row, so the text needs min-width: 0.
      .tmpl-dropdown {
        min-width: 0;

        .k-input-value-text,
        .k-input-inner {
          display: block;
          min-width: 0;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
      }
    }

    // "Will be saved" preview — shown for both Current setup and Copy existing,
    // so either way the user sees the contents before committing to them.
    .tmpl-view-preview {
      // Deliberately NOT its own scroll box. As a shrinkable overflow container
      // it could clip trailing chips behind an inner scrollbar that reads as
      // "that setting isn't being saved". The body is the single scroll region;
      // this block always shows its full contents.
      flex: 0 0 auto;
      margin-bottom: $space-lg;

      .tmpl-view-source-hint {
        margin: 0;
        color: $text-secondary;
        font-size: $font-size-sm;
        line-height: $line-height-md;
        overflow-wrap: anywhere;
      }

      .tmpl-view-summary {
        padding: $space-md;
        border: $border-width-sm solid $border-light;
        border-radius: $border-radius-md;
        background-color: $background-light;

        .tmpl-view-summary-title {
          margin: 0 0 $space-sm;
          color: $text-secondary;
          font-size: $font-size-sm;
          font-weight: $font-weight-medium;
          line-height: $line-height-sm;
          text-transform: uppercase;
          letter-spacing: $tracking-caps;
        }

        .tmpl-view-summary-chips {
          display: flex;
          flex-wrap: wrap;
          gap: $space-xs;
        }

        .tmpl-view-summary-chip {
          display: inline-block;
          max-width: 100%;
          margin-bottom: 0;
          padding: $optical-inset $space-sm;
          border-radius: $border-radius-pill;
          background-color: $background-default;
          border: $border-width-sm solid $border-light;
          color: $text-dark;
          font-size: $font-size-sm;
          font-weight: $font-weight-regular;
          line-height: $line-height-md;
          overflow-wrap: anywhere;
        }
      }
    }

    .tmpl-screen-creater-popup-footer {
      // A full-bleed bar, like the header's divider. The negative inline
      // margins cancel the body's side padding so the rule reaches both edges
      // of the dialog; the matching inline padding puts the buttons back where
      // the body's content column is.
      //
      // The bottom padding lives HERE and not on the body: this footer is
      // `position: sticky`, and sticky offsets from the padding box, so a
      // body padding-bottom (or a negative margin cancelling one) leaves the
      // bar floating short of the dialog's bottom edge.
      margin: auto (-$space-lg) 0;
      padding: $space-md $space-lg $space-lg;
      border-top: $border-width-sm solid $border-light;
      display: flex;
      justify-content: flex-end;
      gap: $space-sm;
      // Stays put at the bottom of the scrolling body, so Create is always
      // reachable no matter how much preview content is above it.
      flex: 0 0 auto;
      position: sticky;
      bottom: 0;
      background-color: $background-default;
    }
  }
}

// Copy-source list. Bounded so a long view list scrolls inside its own popup
// rather than running down the screen, and long names ellipsize.
.tmpl-view-copy-popup {
  .k-list {
    max-height: $view-copy-list-max-height;
  }

  .k-list-item {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.tmpl-dropdown {
  align-items: center;
  background-image: none;
  width: 100%;
  box-shadow: none !important;
  border-radius: $border-radius-md;
  height: $field-height;
}

// ── Buttons ──────────────────────────────────────────────────────────────────
// One box for every button in the package, and it states no height. See the
// `$btn-line` block at the top of this file for what the box is and why the
// line is a length.
//
// These buttons all carry `qo-btn` from their call site, and that is
// load-bearing rather than decorative: the theme scopes its button ink as
// `.qo-btn.qo-text-*` (0,2,0) so it can beat Kendo's own `.k-button-*`. Drop
// `qo-btn` and the colour goes with it.
//
// What they no longer carry is a SIZE class. `qo-btn-md` / `-sm` / `-xs` /
// `-icon` each set `min-height`, side padding and font-size at (0,2,0), and
// `.tmpl-button` was (0,1,0) — it lost all three. That was invisible only
// because the two happened to agree on 40px: the package was reading its height
// off the theme's ramp while appearing to state it, which is how the ramp
// moving under it in theme 2.4.0 inflated the column-chooser rows from ~40px to
// ~61px and stretched the saved-view row icons from 20px to 40px.
//
// The size class is gone from every call site in the package. `.qo-btn.tmpl-*`
// is (0,2,0) and beats the `.qo-btn` base (0,1,0) cleanly, so the box below is
// the only thing that decides it. The variant classes (`qo-btn-primary`,
// `qo-btn-outline-danger`, `qo-text-*`) stay — those are ink, not size.
//
// `min-height: 0` is the part that does the work: the theme's base rule floors
// every button at $control-height, and a floor beats an intrinsic height.

// Text buttons — Advanced Search, Group By, dialog actions, Apply, Clear.
.qo-btn.tmpl-button {
  min-height: 0;
  padding: $btn-inset-y $btn-inset-x;
  font-size: $font-size-sm;
  line-height: $btn-line;
  text-align: center;
  @include tmpl-button-focus;
}

// Icon-only buttons — the four toolbar tools, the filter row's add and remove,
// the saved-view row actions, the criteria box's reset and copy.
//
// The same box with the side inset equal to the vertical one, so the square
// falls out of the glyph instead of being declared. `qo-btn-icon` did this with
// a flat `width: $control-height`, which is the same class of fixed measure as
// the height it replaced.
.qo-btn.tmpl-button-icon {
  min-height: 0;
  padding: $btn-inset-y;
  line-height: $btn-line;
  text-align: center;
  @include tmpl-button-focus;

  // An icon button is square because its four insets are equal and its glyph
  // box is square. Neither dimension is declared on the button itself — the
  // same arithmetic as every other button, with the side inset equal to the
  // vertical one.
  //
  // Kendo derives the glyph box from two custom properties of its own:
  //
  //   .k-button-md.k-icon-button .k-button-icon {
  //     min-width:  calc(var(--kendo-font-size) * var(--kendo-line-height));
  //     min-height: calc(var(--kendo-font-size) * var(--kendo-line-height));
  //   }
  //
  // which resolves to 0.875rem × 1.4285714286 = **20px**, at specificity
  // (0,3,0). Redeclaring the four box properties below ties that and wins only
  // on source order, which is the consuming app's to decide, not ours — and
  // missing one of the four is invisible until it is not: pinning `min-width`
  // but not `min-height` left every icon button 16px wide and 20px tall, so
  // the four toolbar tools drew 32×36 in a 32px row.
  //
  // Setting the two properties Kendo derives FROM is what settles it without
  // fighting: its own calc then lands on $btn-line, whatever the order.
  --kendo-font-size: #{$btn-line};
  --kendo-line-height: 1;

  .k-button-icon,
  .k-svg-icon,
  svg {
    width: $btn-line;
    height: $btn-line;
    min-width: $btn-line;
    min-height: $btn-line;
    font-size: $btn-line;
  }
}

// The toolbar row's own buttons — and the one place in this package where a
// button STATES its height rather than measuring itself.
//
// That is a deliberate exception, not an oversight. Derived height works
// wherever a button only has to look right on its own; it does not work where
// eight controls have to agree to the pixel, because the content the arithmetic
// starts from is a font's line box. Measured on the real row, `line-height +
// padding + border` produced 31.6px for five of the tools and 33.6px for two
// more, against the global search field's declared 32 — the ~0.4px is the gap
// between a stated `line-height` and what the font actually renders, and no
// amount of tuning the inset closes it reliably across two glyph types and a
// Kendo picker.
//
// `box-sizing: border-box` is global (procode-vs-theme's reset), so a declared
// height IS the measured height: padding and border are inside it, and the
// number here is the number in the inspector. The vertical inset goes with it —
// with the box declared it had no job, and leaving it in would have been a
// second, silent measure of the same thing.
//
// Buttons everywhere else in the package still state no height. Nothing outside
// this row has to line up to the pixel with a Kendo control.
.qo-btn.tmpl-button.tmpl-button-field {
  height: $field-height;
  padding: 0 $btn-inset-x;
}

// Square by declaration for the same reason, and squareness is now guaranteed
// rather than inferred from four equal insets around a glyph box that Kendo
// also has opinions about.
.qo-btn.tmpl-button-icon.tmpl-button-field {
  height: $field-height;
  width: $field-height;
  padding: 0;
}

.tmpl-input {
  border-radius: $border-radius-md;
  padding: 0 $space-md;
  box-shadow: none !important;
  height: $field-height;
}

.tmpl-filter-applied {
  border-color: $border-danger !important;
}

// ── Dialog titles ────────────────────────────────────────────────────────────
// The four popups that carry a title — Column Chooser, Select New Group(s),
// Create New View and its Edit View mode — head it with an `<h3 class="qo-h3">`
// and state **no type of their own**.
//
// They used to be `<h2>`s with the type hand-set beside them, three times over:
// `$font-size-xl` / `$line-height-xl` / `$font-weight-semibold` / a -0.2px
// tracking, repeated in each header block at specificity (0,2,1). That is the
// same failure the button box had before 2.9.0 — the package restating a ramp
// it does not own, so the theme could move underneath it and these three would
// not follow. `procode-vs-theme` 2.6.0 gives headings a real scale (`qo-h1` …
// `qo-h6`, size + leading + weight together), which is the thing to name.
//
// **`<h3>` is the level, not just the look.** These are dialogs inside a page
// that already has its own h1; a dialog title is a section of it. The element
// is what a screen reader announces, and `qo-h3` is what it looks like — two
// decisions, kept apart, exactly as the `H1`…`H6` widgets keep them.
//
// Ink is the one declaration left per header, because the theme's heading
// classes set no colour and these popups are portalled out of `.qo-page`, so
// there is nothing to inherit but the document default.
//
// Consequences worth knowing before restyling:
//   - the title renders 16px/1.3/600 rather than 18px/1.25/600, so each header
//     is ~2px shorter; the dialog rhythm above is unchanged and still shared.
//   - a selector written against `.tmpl-chooser-header h2` matches nothing.
