:root{
    /* Both schemes are supported, so the browser paints what it draws itself -
       scrollbars, the open list of a select, autofill - to match. */
    color-scheme:light dark;

    /* Tokens are named after their ROLE, not their colour, so a re-skin
       only touches these values. */

    /* ---- colour ----
       Layer 1 PRIMITIVES: the raw palette, named after the colour itself.
                           Hex values live here and nowhere else.
       Layer 2 SEMANTIC:   what the colour is FOR, built only from primitives.
                           Read as --nu-<position>-<meaning>:
                           position = bg | fg | border
                           meaning  = primary | secondary | tertiary | inverse
                                    | neutral | highlight | warning | alert
                                    | action | scrim | dot | glow
       Components only ever use layer 2. */

    /* ---------- primitives ---------- */
    --nu-white:#ffffff;
    --nu-grey-100:#F1F1F1;
    --nu-grey-400:#A6A6A6;
    --nu-grey-600:#727272;
    /* Two steps that exist for the dark scheme. 800 is the surface raised over
       the page there, at the same quiet distance a tile keeps from white in the
       light one - 1.16:1 against 1.13:1. 700 is the hairline, deliberately a
       shade stronger than its light counterpart, because a rule that quiet
       disappears on a dark ground. */
    --nu-grey-700:#383838;
    --nu-grey-800:#232323;
    --nu-grey-900:#151515;
    /* The two accents that carry meaning on the page ground - an error and a
       warning - exist twice, because they were chosen against white and a dark
       red on a dark page is unreadable. The lighter steps keep the hue and give
       up eight points of saturation, so raising the lightness does not turn them
       garish. Both clear AA on the dark page and on the surface above it. */
    --nu-red-300:#de736d;
    --nu-red-600:#b3261e;
    --nu-burgundy-300:#cf7d92;
    --nu-burgundy-500:#8f2b45;
    /* The top of the burgundy scale: white carrying enough of the 500 to be warm
       rather than pink. It exists so the primary can be as far from a dark page
       as it is from a light one and still be made of the brand's colour. */
    --nu-burgundy-100:#efe1e5;
    --nu-burgundy-700:#5c1228;
    --nu-burgundy-900:#3f0c1b;
    --nu-gold-100:#f7e9b8;
    --nu-gold-600:#9a7b1c;
    --nu-gold-800:#6e5710;
    /* The spectrum the glow is laid out of, one step each and all at the same
       level of the scale, because they are read together as one light rather
       than compared with one another. The red is its own step and not the error
       red: an accent that has to hold its own against four other hues is a
       brighter colour than a message on a white page.

       They sit in the palette because a hex value belongs here and nowhere else.
       Nothing outside the glow uses them. */
    --nu-red-500:#E23B3B;
    --nu-yellow-500:#E8D22D;
    --nu-green-500:#3FB45A;
    --nu-blue-500:#2D8FE8;
    --nu-violet-500:#7B4FE0;

    /* ---------- semantic ---------- */
    /* background – surfaces an element sits on */
    --nu-bg-primary:light-dark(var(--nu-white), var(--nu-grey-900));            /* the page, drawers, menus */
    --nu-bg-secondary:light-dark(var(--nu-grey-100), var(--nu-grey-800));       /* raised panels: tiles, thumbnails */
    --nu-bg-inverse:light-dark(var(--nu-grey-900), var(--nu-grey-100));         /* filled actions */
    /* The ground of the block that stays at the top of the window. The page's
       own colour, thinned: enough for the navigation to hold against whatever
       scrolls behind it, little enough that the shelf stays visible through it.
       It is a wash, not a lid - the work is done by the blur underneath, and a
       solid ground would hide what the blur is there to soften. */
    --nu-bg-masthead:color-mix(in srgb, var(--nu-bg-primary) 70%, transparent);
    /* What a shadow is made of. The same value as the inverse ground while the
       page is light, and a separate name because the two roles part company the
       moment it is dark: the opposite of a dark page is light, but a shadow
       stays dark or stops being a shadow. */
    --nu-bg-shadow:light-dark(var(--nu-grey-900), var(--nu-grey-900));
    /* The five rings of the glow, from the core outwards, named after what each
       one does to the light rather than after its hue: the order is what the eye
       reads, and a ring could be re-toned without the name going stale. They are
       named at all because two marks draw them now, the accent beside the
       wordmark and the bookseller's dot, and a colour kept in two places is a
       colour that will end up being two colours.

       They carry no second value: the halo reads the same on a light page and a
       dark one. Strength is set where the glow is drawn, not here, because the
       same ring at four fifths and at two fifths is what tells working apart
       from ready. */
    --nu-bg-glow-warm:var(--nu-red-500);
    --nu-bg-glow-bright:var(--nu-yellow-500);
    --nu-bg-glow-fresh:var(--nu-green-500);
    --nu-bg-glow-cool:var(--nu-blue-500);
    --nu-bg-glow-dusk:var(--nu-violet-500);
    /* The logotype accent, and the two names exist because of where they are
       used: inside @keyframes. A keyframe resolves light-dark() once, against a
       scheme that is not the one in force, so the dot came out at its dark value
       on a light page. These carry a plain colour instead and are swapped by the
       scheme rules below - a change of value, which does reach an animation. */
    --nu-bg-dot:var(--nu-grey-900);          /* the dot at rest */
    --nu-bg-dot-bloom:var(--nu-white);       /* the dot while the accent blooms */
    /* The bookseller's mark, on the same terms and under its own names. The pair
       of values is the logotype's, so the two look drawn from one colour, and
       the pair of names is not, so a change to one never reaches the other. */
    --nu-bg-ava-dot:var(--nu-grey-900);      /* the bookseller's dot at rest */
    --nu-bg-ava-dot-bloom:var(--nu-white);   /* the bookseller's dot while it blooms */
    --nu-bg-highlight:light-dark(var(--nu-gold-100), var(--nu-gold-100));       /* award badge */
    /* the dim laid over the page behind anything modal: author drawer, cart
       drawer, filter sheet. One name, so the scrims cannot drift apart. */
    --nu-bg-scrim:light-dark(color-mix(in srgb, var(--nu-grey-900) 28%, transparent), color-mix(in srgb, var(--nu-grey-900) 65%, transparent));
    /* the tint that marks a measured distance in the spacing documentation.
       Built from the red rather than the burgundy: burgundy is a muted brand
       colour and thins out grey, which is the one thing this tint must not be. */
    --nu-bg-measure:light-dark(color-mix(in srgb, var(--nu-red-600) 22%, transparent), color-mix(in srgb, var(--nu-red-300) 22%, transparent));
    /* The loudest thing the page can put in front of you, and it is loud by being
       as far from the page as the burgundy goes: nearly the darkest on a white
       page, the lightest on a dark one. One decision mirrored, not two - and the
       label follows on its own, because "text on an inverted ground" already
       means white on a light page and near-black on a dark one. */
    --nu-bg-action:light-dark(var(--nu-burgundy-900), var(--nu-burgundy-100));
    /* derived, not duplicated: the secondary button borrows the panel surface,
       so the two can never drift apart by accident */
    /* The secondary stands one step off the panel it sits on, and the pointer
       takes it one step further in the same direction. On a light page that is
       away from the grey and into the white; on a dark one, away from the dark.
       One sentence, two schemes - and it is the direction that carries the
       meaning, not the colour. */
    --nu-bg-action-secondary:light-dark(
        color-mix(in srgb, var(--nu-grey-400) 6%, var(--nu-white)),
        var(--nu-grey-700));
    --nu-bg-action-secondary-lift:light-dark(
        var(--nu-white),
        color-mix(in srgb, var(--nu-grey-600) 30%, var(--nu-grey-700)));
    /* Where the body goes under the pointer, and it is always further from the
       page: up the burgundy on a light page, on into the white on a dark one,
       where the burgundy has nowhere lighter to go. The aura follows the same
       logic - it has to read against the page rather than sink into it. */
    --nu-bg-action-glow:light-dark(var(--nu-burgundy-500), var(--nu-white));
    --nu-bg-action-glow-deep:light-dark(var(--nu-burgundy-700), var(--nu-burgundy-500));

    /* foreground – text and icons */
    --nu-fg-primary:light-dark(var(--nu-grey-900), var(--nu-grey-100));         /* titles, body copy, prices, icons */
    --nu-fg-secondary:light-dark(var(--nu-grey-600), var(--nu-grey-400));       /* authors, labels, quotes, notes */
    --nu-fg-tertiary:light-dark(var(--nu-grey-400), var(--nu-grey-600));        /* disabled or less prominent text */
    --nu-fg-inverse:light-dark(var(--nu-white), var(--nu-grey-900));            /* copy on an inverse background */
    --nu-fg-highlight:light-dark(var(--nu-gold-800), var(--nu-gold-800));       /* award label */
    --nu-fg-warning:light-dark(var(--nu-burgundy-500), var(--nu-burgundy-300));     /* inventory running out */
    --nu-fg-alert:light-dark(var(--nu-red-600), var(--nu-red-300));            /* failed validation */

    /* border – outlines and separators */
    --nu-border-neutral:light-dark(var(--nu-grey-100), var(--nu-grey-700));     /* hairlines, field borders, dividers */
    --nu-border-primary:light-dark(var(--nu-grey-900), var(--nu-grey-100));     /* focus rings, selected chip, badge */
    --nu-border-muted:light-dark(var(--nu-grey-400), var(--nu-grey-600));       /* receded outline: unavailable badge */
    --nu-border-hover:light-dark(var(--nu-grey-400), var(--nu-grey-600));       /* an outline answering the pointer */
    --nu-border-highlight:light-dark(var(--nu-gold-600), var(--nu-gold-600));   /* award badge */
    --nu-border-warning:light-dark(var(--nu-burgundy-500), var(--nu-burgundy-300)); /* low-stock badge */
    --nu-border-alert:light-dark(var(--nu-red-600), var(--nu-red-300));        /* invalid field */

    /* ---- type ---- */
    --nu-font-display:"DM Serif Display",Georgia,serif;   /* identity & headings */
    --nu-font-text:"Archivo","Helvetica Neue",Helvetica,Arial,sans-serif;

    /* Size on its own, named by size. A step says how big, never what for –
       what for is the business of a style, which points at one of these. Two
       styles sharing a step today part company by pointing at different ones,
       not by the scale growing a second value of the same size. */
    --nu-text-size-xs:0.625rem;
    --nu-text-size-sm:0.6875rem;
    --nu-text-size-md:0.8125rem;
    --nu-text-size-lg:1.125rem;
    --nu-text-size-xl:1.5rem;
    --nu-text-size-2xl:1.875rem;

    /* line height – six steps, named after how much air the line needs rather
       than after the number. A step is a ratio, so it holds at every size. */
    --nu-line-flat:1;        /* the box sets the height: badge, counter, wordmark */
    --nu-line-tight:1.15;    /* display sizes, where the leading would gape */
    --nu-line-snug:1.35;     /* a title inside a list row */
    --nu-line-normal:1.5;    /* running copy and the text inside controls */

    /* two weights, and only two: the text family ships these and nothing else */
    --nu-weight-normal:400;
    --nu-weight-medium:500;

    /* A named style, packed into one value for the font shorthand: weight, size,
       line height and family in the order that property expects. A rule says
       which style it is instead of assembling four properties itself.

       Two things the shorthand cannot carry. Letter-spacing is not part of it,
       so a style that needs one declares it beside. And the shorthand resets
       every font property it does not mention – font-style and
       font-variant-numeric among them – so where a rule wants italics or
       tabular figures, the shorthand goes first and the exception after it. */
    --nu-type-h1:var(--nu-weight-normal) var(--nu-text-size-2xl)/var(--nu-line-tight) var(--nu-font-display);
    --nu-type-h2:var(--nu-weight-normal) var(--nu-text-size-xl)/var(--nu-line-tight) var(--nu-font-display);
    --nu-type-h3:var(--nu-weight-normal) var(--nu-text-size-lg)/var(--nu-line-tight) var(--nu-font-display);
    --nu-type-body-l:var(--nu-weight-normal) var(--nu-text-size-lg)/var(--nu-line-normal) var(--nu-font-text);
    --nu-type-body-m:var(--nu-weight-normal) var(--nu-text-size-md)/var(--nu-line-normal) var(--nu-font-text);
    /* Small running text: a field's label, its message, the strapline, and the
       uppercase group headings, which are this style with tracking and capitals
       added by the rule - the font: shorthand carries neither, so a second token
       named after them would promise what it cannot deliver. Normal leading,
       because any of these can break onto a second line. */
    --nu-type-body-s:var(--nu-weight-normal) var(--nu-text-size-sm)/var(--nu-line-normal) var(--nu-font-text);
    --nu-type-label:var(--nu-weight-normal) var(--nu-text-size-sm)/var(--nu-line-flat) var(--nu-font-text);
    --nu-type-caption:var(--nu-weight-normal) var(--nu-text-size-xs)/var(--nu-line-flat) var(--nu-font-text);

    /* letter-spacing – scales against size and length, not case */
    --nu-tracking-display:-.017em; /* large display type: the wordmark */
    --nu-tracking-body:0;          /* running copy: never spaced out */
    --nu-tracking-compact:.04em;   /* short strings of any case: buttons, labels, badges */
    --nu-tracking-caps:.1em;       /* the smallest uppercase: section heads, group heads */

    /* The gap between a word and the rule under it. In px, and deliberately: a
       hairline's distance from a hairline is not measured against the text, so
       it does not grow with it. Below the spacing scale, which starts four
       times higher. */
    --nu-underline-offset:1px;

    /* ---- spacing ----
       One rhythm, no exceptions: every gap, padding, margin and inset is a
       multiple of 4px.

       The names order the steps from smallest to largest without binding any of
       them to an index. Dropping a step is then: point its users at a neighbour,
       delete the line. Nothing else shifts, and no hole is left behind – a
       numbered scale would read 1,2,3,4,6,8,10,12,16 afterwards, which looks
       like a mistake and invites a renumber nobody needs to risk.

       Two kinds of spacing sit outside the scale, both on purpose:

       HAIRLINES. Every literal px left in this sheet is 1px – a label held off
       its own 1px underline, a 1.5px bar placed inside a 10px icon, a glyph
       pulled onto its baseline. Optical correction under the grid's resolution;
       the smallest step is four times too coarse for it.

       PROPORTIONAL SPACE. A few gaps have to grow with something else instead
       of holding a fixed size: the dot beside the wordmark is offset in em, so
       it follows the logo's type size, and the inside of a drawn cover is
       padded in %, so the same jacket reads identically at grid size and at
       packshot size. A fixed step would cut those ties.

       Everything else – space between two separate things, at a size of its
       own – comes off the scale. */
    --nu-space-nano:0.25rem;
    --nu-space-micro:0.5rem;
    --nu-space-milli:0.75rem;
    --nu-space-small:1rem;
    --nu-space-medium:1.5rem;
    --nu-space-large:2rem;
    --nu-space-xlarge:2.5rem;
    --nu-space-huge:3rem;
    --nu-space-max:4rem;

    /* Motion. Named by the job, like the spacing scale, so a value can move
       between steps without every rule being renamed. Instant is under the
       threshold where a change reads as a move; quick answers in place; base
       carries colour and a small turn; slow is for something arriving or
       leaving; slower is for the two moments that cross the page. */
    /* The focus ring: one thickness everywhere, and two distances. Outward is the
       normal case; inward is for a control sitting flush inside another one's
       outline, where there is no room outside for a ring to stand. */
    --nu-focus-ring:1.5px;
    --nu-focus-offset:2px;
    --nu-focus-offset-inset:-2px;

    --nu-motion-instant:90ms;
    --nu-motion-quick:150ms;
    --nu-motion-base:250ms;
    --nu-motion-slow:350ms;
    --nu-motion-slower:550ms;
    --nu-motion-loop:26s;
    /* The starts of the mosaic are spread over this, not the length of one card. */
    --nu-motion-stagger:900ms;
    /* Neither is it a duration of movement: this is how long a confirmation
       stays on screen once the movement has finished, and how long a reader is
       left alone with a view before anything speaks to her unasked. Two
       waits rather than two names: both are the pause the shop keeps around
       something the reader did not ask to see. */
    --nu-motion-hold:1800ms;
    /* Two curves that are decisions rather than defaults: a panel sliding into
       place, and a tile growing into a packshot. Plain ease and linear stay
       keywords. */
    --nu-ease-slide:cubic-bezier(.32,.72,.27,1);
    --nu-ease-zoom:cubic-bezier(.22,.8,.2,1);

    /* Blur. Three steps, named by their own measure like the colour and text
       scales, because the job here is always the same one: something is not yet
       in focus, or is no longer what the reader is looking at. How much a thing
       can afford to lose is what sets the step, and a picture can afford least.
       Small: a cover arriving, which has to stay recognisable on the way in.
       Middle: a whole view put behind a layer, held down because a reader who
       cannot tell what is underneath loses track of where she will come back to.
       Large: text arriving, where a word may be illegible for a moment and
       nothing is lost by it.
       Extra large: the top of the ramp under the block that stays at the top of
       the window. It can go this far precisely because it is a strip and not a
       view - everything below it stays sharp, so nothing is asked of what passes
       behind except that it stop competing for the eye. */
    --nu-blur-sm:6px;
    --nu-blur-md:8px;
    --nu-blur-lg:12px;
    --nu-blur-xl:32px;

    /* layout */
    /* Icon sizes. Container is what the icon occupies on the page; inset is the
       margin the drawing keeps inside it, stroke included; stroke is measured in
       grid units, which equal pixels because the viewBox equals the container.
       Named rather than written into the classes, so the documentation can read
       the rule instead of restating it. */
    /* Relative width of a cover inside its tile. One decision per breakpoint:
       opening a book zooms one and the same box from the grid to the product
       page, so the two have to agree at every width or the transition snaps. */
    --nu-cover-width:62%;
    /* A cover standing on its own, outside a tile. Two sizes of one thing: the
       cart drawer is narrower than the cart page. In rem because the thumbnail
       shares its row with a title and a price, and a row where only the text grows
       stops belonging to the cover at all. */
    --nu-thumb-size-sm:2.75rem;  --nu-thumb-size-md:4rem;
    /* A round mark standing for a person: the author in her drawer, the
       bookseller in her corner. Two sizes, because the drawer has a column to
       fill and the corner has to stay out of the way. In rem like the thumbnail
       and for the same reason - the mark shares its box with type, and a box
       where only the text grows stops belonging to the mark inside it. */
    --nu-avatar-size-sm:3rem;    --nu-avatar-size-md:6rem;
    /* The stepper's button, sized by the icon it holds rather than by a finger:
       the icon is in rem, so the button has to be too, or the two drift apart the
       moment a reader enlarges text. WCAG asks for 24x24 CSS px, which this clears
       at the default root size and only exceeds above it. It also sets the width of
       the value between the two buttons, so the control does not resize between
       9 and 10. */
    --nu-control-size-sm:1.625rem;
    /* A field holding up to four characters: a building number, a flat number,
       a dialling code. One value for all three, so fields that stand in a row
       line up. The width is set by the widest case, a select - it holds the four
       characters, the gap and the chevron - and the plain fields follow it. */
    --nu-field-width-short:6.5rem;
    /* The search field has two widths. At rest it is as narrow as its own
       placeholder allows, so it sits in the bar as one control among three
       rather than as the bar's main event. Holding the focus it takes the room
       it needs to show a long title back to the reader. */
    --nu-field-width-search:13rem;
    --nu-field-width-search-focus:22rem;

    /* The row standing above the content, before the reader reaches what she
       came for. Every view has one and each holds one control on the left: the
       filter toggle in the shop, the way back on a product. They have to be the
       same height, because moving between the views leaves that control where it
       was - and it is the one thing on screen that does not change its job, so a
       reader notices it moving before she notices anything else.
       Built from the search field, which is the tallest thing either row holds
       and therefore what sets the height over the grid anyway: its padding twice
       over, one line of text, and the rule it draws underneath. Written once
       here rather than restated wherever a row needs it, because two copies of a
       height drift the first time the field changes. */
    --nu-bar-height:calc(var(--nu-space-milli) * 2 + var(--nu-line-normal) * 1em + 1px);

    /* The inset is the margin between the container and the safe area the drawing
       has to fit inside. No rule in the sheet reads it: the documentation does,
       building the name from the size it is describing, so a search for the
       literal name finds nothing and the token looks dead. It is not. */
    --nu-icon-lg:1.5rem;   --nu-icon-lg-inset:2px; --nu-icon-lg-stroke:1.5;
    --nu-icon-sm:1rem;     --nu-icon-sm-inset:1px; --nu-icon-sm-stroke:1.25;

    /* An icon set beside a word is centred against the word's box, and that box
       is taller than the marks inside it: the leading and the descender space
       sit under the letters, so its middle falls below the middle of the word.
       The icon is lifted back by this much. Judged by eye, which is what an
       optical correction is, and in px because it corrects a mark against a
       mark rather than a length against the text. */
    --nu-icon-lift:2px;

    --nu-form-max-width:1440px;      /* cart & checkout: grows, then stops and centres */
    /* One gutter for every side-by-side layout: shop, product, checkout, docs.
       Named rather than repeated, because "how far apart do two columns sit" is
       a single decision – four copies of it could drift without anyone noticing. */
    --nu-gutter-column:var(--nu-space-huge);

    /* Height of the mobile sticky bars. Fallbacks only – the real values are
       measured and written back by the script, so the page clearance is always
       exactly the bar, whatever the label length or the device safe area. */
    --nu-mobar-height:var(--nu-space-max);  /* shop bar: filters + sort */
    /* Ta sama belka bez zaokrąglenia do siatki. Zaokrąglenie w górę jest po to,
       żeby prześwit pod treścią nigdy nie był za mały &ndash; ale cokolwiek ma na
       belce usiąść, musi znać jej prawdziwą wysokość, bo inaczej zostawia pod
       sobą do czterech pikseli tła. Czyta to arkusz filtrów na telefonie. */
    --nu-mobar-height-exact:var(--nu-space-max);
    --nu-cobar-height:var(--nu-space-max);  /* checkout bar: total + submit */
    /* The block that stays at the top of the window. Same contract as the two
       above: a starting value the script replaces with the measured height, and
       it does so on the first route, before a reader can reach the one thing
       that reads it. */
    --nu-masthead-height:var(--nu-space-max);
    --nu-mobar-padding:var(--nu-space-milli); /* pinned bar: padding under it, and the gap between its two rows */
    --nu-mobar-gap:var(--nu-space-milli); /* air between the bar and the menu above it */
  }

/* ----------------------------------------------------------------- scheme
   Every semantic colour that differs between the two schemes is declared once
   and carries both values: light-dark() picks by the used color-scheme, so the
   shop follows the reader's system by default and the switch in the header has
   only one property to set instead of twenty-eight names to redeclare. The four
   that carry no second value are the two marks that bloom and their bloom, and
   they are redeclared in the blocks below rather than paired here, because a
   value a keyframe reads has to be a plain colour: light-dark() does not
   resolve inside @keyframes.

   It is not an inversion. The greys fold over onto themselves, but the two
   accents that carry meaning need their lighter steps, because a dark red on a
   dark page is unreadable. The award badge keeps its pale ground either way. */
@media (prefers-color-scheme:dark){
  :root{
    --nu-bg-dot:var(--nu-grey-100);--nu-bg-dot-bloom:var(--nu-grey-900);
    --nu-bg-ava-dot:var(--nu-grey-100);--nu-bg-ava-dot-bloom:var(--nu-grey-900);
  }
}
:root[data-scheme="light"]{
  color-scheme:light;
  --nu-bg-dot:var(--nu-grey-900);--nu-bg-dot-bloom:var(--nu-white);
  --nu-bg-ava-dot:var(--nu-grey-900);--nu-bg-ava-dot-bloom:var(--nu-white);
}
:root[data-scheme="dark"]{
  color-scheme:dark;
  --nu-bg-dot:var(--nu-grey-100);--nu-bg-dot-bloom:var(--nu-grey-900);
  --nu-bg-ava-dot:var(--nu-grey-100);--nu-bg-ava-dot-bloom:var(--nu-grey-900);
}

/* Nothing is pinned here any more. Six names used to be, for both solid buttons,
   because the glass resolved half the palette inside itself. A flat colour
   resolves one thing, and once the primary began mirroring the page - darkest
   burgundy on white, lightest on dark - even that one came right on its own:
   --nu-fg-inverse already means white on a light page and near-black on a dark
   one, which is exactly what a button that inverts the page needs. */
  *{box-sizing:border-box;margin:0;padding:0}
  [hidden]{display:none !important}
  html{ -webkit-text-size-adjust:100%; }
  body{
    background:var(--nu-bg-primary);
    color:var(--nu-fg-primary);
    /* the page's own style, which everything inherits until a rule says otherwise */
    font:var(--nu-type-body-m);
    /* stated rather than left to the default: running copy carries no tracking,
       and a token nothing references is a decision nothing enforces */
    letter-spacing:var(--nu-tracking-body);
    -webkit-font-smoothing:antialiased;
  }
  /* The browser default for emphasis is 700, which the text family does not
     ship – it would be synthesised from 400 and smear. 500 is the one heavier
     weight the shop loads, so emphasis uses it. */
  strong,b{font-weight:var(--nu-weight-medium)}
  button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
  a{color:inherit;text-decoration:none}

  /* ---------- header ---------- */
  /* The one inverse surface in the shop. The code inside is a ghost button -
     an action taken in place, woven into the sentence - so it keeps the ghost
     construction and only swaps the two colours for their inverse counterparts. */
  /* Hidden until the Tab key reaches it, then a plain box in the top corner.
     Not visually-hidden-forever: a link nobody can see is a link nobody uses,
     and the point of this one is that it announces itself on the first Tab. */
  .skip-link{
    position:absolute;left:var(--nu-space-small);top:var(--nu-space-small);z-index:100;
    width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;
  }
  .skip-link:focus{
    width:auto;height:auto;clip-path:none;
    padding:var(--nu-space-micro) var(--nu-space-small);
    background:var(--nu-bg-primary);color:var(--nu-fg-primary);
    border:1px solid var(--nu-border-primary);
    outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset);
  }
  /* The landing spot takes focus from the link and gives it straight back to the
     page: it is a position, not a control, so it never draws a ring. */
  #content:focus{outline:none}
  /* What stays at the top of the window: the promotion, the navigation and, on a
     product page, the way out of it. One sticky element instead of three,
     because three would each need to know how tall the ones above them are, and
     this one is simply as tall as whatever it holds - the row is hidden off the
     product view and the block gets shorter.
     The controls over the grid are deliberately outside it. A field is for
     typing into and reads worst of all while the shelf slides behind it, so the
     bar holding it stays with the shelf it narrows.
     Not opaque: what passes behind it stays there, out of focus rather than out
     of sight, so the reader keeps seeing how much shelf is above her. The wash
     of page colour is only as much as the navigation needs to be legible over
     whatever happens to be underneath. The band below carries the same blur on
     to nothing, so the edge is a passage and not a line.
     Above the filter sheet's backdrop and below the drawers, which are meant to
     cover it. */
  .masthead{position:sticky;top:0;z-index:80}
  /* Behind everything the block holds and in front of the page moving under it:
     a negative step inside the block's own stacking context. Reaches one step of
     the scale below the block, which is where the ramp runs out. */
  /* At rest the veil is the block and nothing more: behind the block there is
     only the page, so there is nothing to blur, and a ramp reaching into the
     content would soften the first row of covers for no reason. The reach opens
     the moment something is actually passing behind, and closes again at the
     top of the page. */
  .masthead-veil{
    position:absolute;left:0;right:0;top:0;bottom:0;
    transition:bottom var(--nu-motion-quick) ease;
    z-index:-1;pointer-events:none;
    background:linear-gradient(to top, transparent 0%, transparent 45%, var(--nu-bg-masthead) 88%);
  }
  body.is-scrolled .masthead-veil{bottom:calc(-1 * var(--nu-space-max))}
  .masthead-veil span{position:absolute;inset:0}
  /* Four layers, and the ramp between them is not even. Through the lower half
     the blur barely moves - a cover on its way up is still a cover, and softening
     it early would only make the shelf look out of focus. Past the middle it
     quadruples in one step and doubles again, so the last stretch before the
     block is where everything goes. One token sets all four: the others are it,
     divided down.
     The wash follows the same shape, staying out of the way until the middle and
     arriving with the blur. */
  .masthead-veil span:nth-child(1){
    -webkit-backdrop-filter:blur(calc(var(--nu-blur-xl) / 16));backdrop-filter:blur(calc(var(--nu-blur-xl) / 16));
    -webkit-mask-image:linear-gradient(to top, transparent 0%, #000 25%);
    mask-image:linear-gradient(to top, transparent 0%, #000 25%);
  }
  .masthead-veil span:nth-child(2){
    -webkit-backdrop-filter:blur(calc(var(--nu-blur-xl) / 8));backdrop-filter:blur(calc(var(--nu-blur-xl) / 8));
    -webkit-mask-image:linear-gradient(to top, transparent 25%, #000 50%);
    mask-image:linear-gradient(to top, transparent 25%, #000 50%);
  }
  .masthead-veil span:nth-child(3){
    -webkit-backdrop-filter:blur(calc(var(--nu-blur-xl) / 2));backdrop-filter:blur(calc(var(--nu-blur-xl) / 2));
    -webkit-mask-image:linear-gradient(to top, transparent 50%, #000 70%);
    mask-image:linear-gradient(to top, transparent 50%, #000 70%);
  }
  .masthead-veil span:nth-child(4){
    -webkit-backdrop-filter:blur(var(--nu-blur-xl));backdrop-filter:blur(var(--nu-blur-xl));
    -webkit-mask-image:linear-gradient(to top, transparent 70%, #000 88%);
    mask-image:linear-gradient(to top, transparent 70%, #000 88%);
  }
  /* The skip link lands under the block, so the block must not cover where it
     lands. Measured rather than added up here: the height depends on which view
     is showing. */
  #content{scroll-margin-top:var(--nu-masthead-height)}

  .promo{
    background:var(--nu-bg-inverse);color:var(--nu-fg-inverse);
    padding:var(--nu-space-micro) var(--nu-space-small);
    text-align:center;
  }
  .promo p{margin:0}
  /* The one control in the shop standing on an inverse ground, so the tertiary
     button's two colours are swapped for their opposites: full strength is the
     inverse foreground, and lightening under the pointer is that same colour
     thinned rather than a lighter grey, which would disappear into the bar. */
  .promo .btn-tertiary{color:var(--nu-fg-inverse)}
  .promo .btn-tertiary:hover{color:color-mix(in srgb, var(--nu-fg-inverse) 70%, transparent)}
  .promo .btn-tertiary:focus-visible{outline-color:var(--nu-fg-inverse)}
  .promo-done{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .promo-done.is-visible{
    position:static;width:auto;height:auto;clip-path:none;
    margin-left:var(--nu-space-micro);color:color-mix(in srgb, var(--nu-fg-inverse) 70%, transparent);
  }

  header{
    display:flex;align-items:center;justify-content:space-between;
    padding:var(--nu-space-medium) var(--nu-space-small);
  }
  .logo{display:flex;align-items:baseline;gap:var(--nu-space-milli)}
  .logo .mark{
    /* The wordmark is the one place with its own leading: at this size a heading's
       leading would leave the dot floating, so the line is set flat and the style
       is assembled here rather than taken from --nu-type-h1. */
    font-family:var(--nu-font-display);font-weight:var(--nu-weight-normal);
    font-size:var(--nu-text-size-2xl);line-height:var(--nu-line-flat);letter-spacing:var(--nu-tracking-display);
    display:inline-flex;align-items:baseline;
    /* Its own painting context, so the accent's z-index:-1 puts the dot under
       the word instead of under the page. It used to borrow one from the filter
       on the dot; the moment the filter went, so did the dot. */
    position:relative;isolation:isolate;
  }
  .logo .mark::after{
    content:"";
    display:inline-block;
    width:.2em;height:.2em;border-radius:50%;
    /* Relative, so the gap tracks the type size: the same value holds at the
       header's two sizes, which a fixed pixel added to it did not - it weighed
       proportionally more on the smaller logo. */
    margin-left:.09em;
    position:relative;z-index:-1;
    transform-origin:center;
    background:var(--nu-bg-dot);
    animation:dotBloom var(--nu-motion-loop) ease-in-out infinite;
  }
  /* The glow is a box-shadow, not a drop-shadow. A filter rasterises the element
     it runs on: at six pixels across that shows as a ragged edge and a clipped
     right side. A shadow on a border-radius circle draws the same halo and stays
     vector. The second set of hues is the first one reordered, which reads as
     the hue drifting rather than as a different light.

     A stop with no glow says transparent rather than naming a hue at nothing:
     the colour is interpolated with its alpha already applied, so a hue at zero
     and plain transparent take the same path to full strength, and transparent
     is the shorter way of saying there is nothing there. */
  @keyframes dotBloom{
    /* resting: plain dot, no glow - most of the cycle */
    0%, 74% {
      background:var(--nu-bg-dot);transform:scale(1);
      box-shadow:
        0 0 4px transparent,
        0 0 8px transparent,
        0 0 12px transparent,
        0 0 16px transparent,
        0 0 20px transparent;
    }
    /* accent blooms in */
    81% {
      background:var(--nu-bg-dot-bloom);transform:scale(1);
      box-shadow:
        0 0 4px color-mix(in srgb, var(--nu-bg-glow-warm) 85%, transparent),
        0 0 8px color-mix(in srgb, var(--nu-bg-glow-bright) 80%, transparent),
        0 0 12px color-mix(in srgb, var(--nu-bg-glow-fresh) 80%, transparent),
        0 0 16px color-mix(in srgb, var(--nu-bg-glow-cool) 80%, transparent),
        0 0 20px color-mix(in srgb, var(--nu-bg-glow-dusk) 80%, transparent);
    }
    /* held a little while, hues drifting */
    91% {
      background:var(--nu-bg-dot-bloom);transform:scale(1);
      box-shadow:
        0 0 4px color-mix(in srgb, var(--nu-bg-glow-dusk) 85%, transparent),
        0 0 8px color-mix(in srgb, var(--nu-bg-glow-cool) 80%, transparent),
        0 0 12px color-mix(in srgb, var(--nu-bg-glow-warm) 80%, transparent),
        0 0 16px color-mix(in srgb, var(--nu-bg-glow-bright) 80%, transparent),
        0 0 20px color-mix(in srgb, var(--nu-bg-glow-fresh) 80%, transparent);
    }
    /* recede back to the plain dot */
    97%, 100% {
      background:var(--nu-bg-dot);transform:scale(1);
      box-shadow:
        0 0 4px transparent,
        0 0 8px transparent,
        0 0 12px transparent,
        0 0 16px transparent,
        0 0 20px transparent;
    }
  }
  @media (prefers-reduced-motion:reduce){
    .logo .mark::after{animation:none;background:var(--nu-bg-dot);box-shadow:none;transform:none}
  }
  /* The wordmark links home and needs the ring every other control keeps. It
     takes nothing else from .link: the accent's colour comes from the keyframes,
     so dimming the word under the pointer would leave the dot behind. */
  .logo .mark:focus-visible{
    outline:var(--nu-focus-ring) solid var(--nu-border-primary);
    outline-offset:var(--nu-focus-offset);
  }
  .logo .strap{
    font:var(--nu-type-body-s);color:var(--nu-fg-secondary);
  }
  /* Once covers start passing behind the block, one fixed grey is legible over
     some of them and gone over others. Inverting instead makes every pixel of
     the line the page minus itself, recomputed as the covers move, so there is
     no backdrop it was not chosen for. The value stays the one the line stands
     in at rest, because subtracting it from an untouched page gives back the
     same strength the line already had - what changes is that the strength now
     holds over anything.
     It has one blind spot, where the difference lands on the colour it is being
     taken from. On a light page the wash keeps everything behind well clear of
     it; on a dark one it sits at the edge of the range. */
  body.is-scrolled .logo .strap{
    mix-blend-mode:difference;color:var(--nu-fg-secondary);
  }
  .head-right{display:flex;align-items:center;gap:var(--nu-space-xlarge)}
  /* Two blocks, not three controls in a row: the pair that decides what the
     shop says stands together, and the one that decides how it looks stands a
     step away - the same step the switchers keep from the header's icons. */
  .switchers{display:flex;align-items:center;gap:var(--nu-space-xlarge)}
  .sw-content{display:flex;align-items:center;gap:var(--nu-space-small)}
  /* Layout only. The two options inside are chips: a pair of mutually exclusive
     facets reporting through aria-pressed, which is the chip's definition. Only
     the separator between them keeps the group's own colour. */
  .sw-group{display:flex;align-items:center;gap:var(--nu-space-nano);color:var(--nu-fg-secondary)}
  /* Two icon sizes, and only two. Everything an icon is - the box, the stroke,
     the fill, where the colour comes from - is declared here and nowhere else,
     so a component picks a size and inherits the rest. The small size is the
     large drawing at 0.7, which maps the 20x20 safe area exactly onto 14x14. */
  .ico-lg,
  .ico-sm{display:block;flex:none;stroke:currentColor;fill:none}
  .ico-lg{width:var(--nu-icon-lg);height:var(--nu-icon-lg);stroke-width:var(--nu-icon-lg-stroke)}
  .ico-sm{width:var(--nu-icon-sm);height:var(--nu-icon-sm);stroke-width:var(--nu-icon-sm-stroke)}

  .head-icons{display:flex;gap:var(--nu-space-medium);align-items:center}

  /* ---------- tertiary button ----------
     Reveals or hides a region and says so through aria-expanded. Starts at full
     strength and lightens under the pointer – it has nowhere darker to go, and the
     link shares that register. Carries a leading icon, a trailing one, or an icon
     on its own; the gap is the only thing an icon adds. */
  /* No cursor here, nor on the ghost or the link: the hand comes from the
     element being a button, which every instance in the shop is. Declaring it on
     the class would also hand it to a specimen in these pages, which does
     nothing when clicked. */
  .btn-tertiary{
    display:inline-flex;align-items:center;gap:var(--nu-space-micro);
    color:var(--nu-fg-primary);
    /* see the note on .has-icon: an inline-flex box with a leading icon would
       otherwise be placed by a baseline invented at the glyph's bottom edge */
    vertical-align:middle;
  }
  .btn-tertiary:hover{color:var(--nu-fg-secondary)}
  .btn-tertiary:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}

  /* ---------- ghost button and link ----------
     What separates them is what happens, not how they look at first glance: a
     ghost button does something on the spot – opens the drawer, removes a line –
     while a link leaves for another view. So the button wears its underline all
     the time, the way a control does, and the link only takes one under the
     pointer. Both declare it at rest, so colouring one moves no text. They part
     company on colour as well: the ghost starts quiet and darkens, the link starts
     at full strength and lightens, the way the tertiary button does. */
  /* The mark sits on the word, never on the control. A border on the button
     itself lands under the whole line box, half-leading included, and the same
     token then reads three pixels further from the text than it does on a chip.
     An inline span draws its border around the letters, which is what every
     other underline in the shop does. */
  .btn-ghost{color:var(--nu-fg-secondary)}
  .btn-ghost .lbl{border-bottom:1px solid var(--nu-border-neutral);padding-bottom:var(--nu-underline-offset)}
  .btn-ghost:hover{color:var(--nu-fg-primary)}
  .btn-ghost:hover .lbl{border-bottom-color:var(--nu-border-primary)}
  .btn-ghost:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  /* one glyph at a time: the check replaces the sheets once the copy succeeded */
  .ico-check{display:none}
  .is-copied .ico-copy{display:none}
  .is-copied .ico-check{display:block}
  /* Full strength at rest and lightening under the pointer, the same register as
     the tertiary button: both are ways out of where you are, so they read alike.
     Two variants, and the difference is where the link stands. Standing on its
     own - a footer, a way back - it is found by its place, so colour is the whole
     treatment. Standing inside a sentence it has to be found among words, and
     colour alone is what WCAG 1.4.1 does not allow, so there the underline is
     permanent rather than waiting for a pointer that a reader may never move.
     It follows the text through currentColor rather than naming a border token,
     which would leave a dark rule under lightened type. The in-text variant does
     not take an icon: a glyph inside a sentence reads as punctuation, and the
     rule would have to run under it or stop short of it, neither of which looks
     like an underlined word. */
  .link{color:var(--nu-fg-primary)}
  .link:hover{color:var(--nu-fg-secondary)}
  .link.in-text{border-bottom:1px solid currentColor;padding-bottom:var(--nu-underline-offset)}
  .link:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  /* A link with an icon is built exactly as a ghost with one: the underline moves
     onto the label so it does not run under the glyph, and the icon is a drawn
     SVG rather than a typed arrow, so it follows the colour and the size of the
     control instead of the font. The two differ only where they already differ -
     the ghost's underline is standing, the link's is transparent until hover. */
  .link.has-icon{
    display:inline-flex;align-items:center;gap:var(--nu-space-micro);
    /* An inline-flex box takes its baseline from its first child, and an SVG has
       none - so with a leading icon the browser invents one at the glyph's bottom
       edge and reserves descender space beneath it, making the line taller than
       the control. vertical-align stops it being placed by that invented baseline,
       so the two arrangements - icon first, icon last - behave alike. */
    vertical-align:middle;
  }
  /* Wherever an icon stands beside a word - and .has-icon is the name of that
     arrangement, whichever control carries it - the icon is lifted off the
     centre of the text's box onto the centre of the word itself. */
  .has-icon svg{margin-bottom:var(--nu-icon-lift)}
  /* Every control that answers the pointer with colour answers over one step,
     the same one the two solid buttons use: quick is the scale's name for an
     element answering under the pointer without leaving its place. Underlines
     sit on the label in the variants that carry an icon and in the chip and the
     sort item, so those labels are named here rather than their buttons. Not
     withdrawn under prefers-reduced-motion: nothing here moves, and the sheet
     only drops transform for a reader who asked for less motion. */
  .btn-ghost, .btn-ghost .lbl,
  .link,
  .btn-tertiary,
  .chip .chip-t,
  .sort-menu .lbl,
  .ds-nav-item{
    transition:color var(--nu-motion-quick) ease, border-color var(--nu-motion-quick) ease;
  }
  /* The one that answers with its ground rather than its text. */
  .sort-menu button{transition:background-color var(--nu-motion-quick) ease}

  /* ---------- shop bar (filter toggle + sort) ---------- */
  /* Three columns, and the outer two are equal: that puts the field in the middle
     of the page rather than in the middle of what the two labels leave over. With
     the flex row it drifted left, because "Sortuj: Nasze rekomendacje" is far
     longer than "Filtry" and the label changes length with the language. */
  /* The row above the content. Every view has one and each holds the control
     that view offers there - the filters over the grid, the way out of a book.
     Height and padding are one decision for both, so the control does not move
     when the reader does. What differs is only how many things stand in the row. */
  .shopbar,.productbar{
    min-height:var(--nu-bar-height);align-items:center;
    padding:0 var(--nu-space-small) var(--nu-space-small);
  }
  .productbar{display:flex}
  .shopbar{
    display:grid;grid-template-columns:1fr auto 1fr;
    gap:var(--nu-space-medium);
  }
  .shopbar .filter-toggle{justify-self:start}
  .shopbar .sort{justify-self:end}
  .filter-toggle{padding:var(--nu-space-nano) 0}
  /* ---------- tertiary button icons ----------
     One construction for all of them: two 1.5px bars in a shared box, drawn in
     currentColor so they follow the button into hover. A plus is the pair at
     right angles and a cross is the same pair turned 45 degrees, which is why
     the sort button can rotate one into the other. Drawn rather than set as
     glyphs, because a glyph's visual size is a fraction of its font-size that
     depends on the typeface – that is how three icons ended up three sizes. */
  /* The tertiary marks, drawn as SVG on the 16 grid like every other icon. The
     filter's lower bar reports the panel's state: it runs full width while the
     panel is closed and shortens once it opens. Scaling it horizontally leaves
     the stroke alone, since a horizontal rule's weight is its vertical extent. */
  .ico-filter .bar-bot{
    transform-box:view-box;transform-origin:2px 11px;
    transform:scaleX(.62);transition:transform var(--nu-motion-base) ease;
  }
  .btn-tertiary[aria-expanded="false"] .ico-filter .bar-bot{transform:scaleX(1)}
  /* Plus and close are one drawing: the cross is the plus turned 45 degrees.
     Sized so the rotated form still clears the safe area. */
  .ico-plus{transition:transform var(--nu-motion-base) ease}
  .ico-close{transform:rotate(45deg)}
  .btn-tertiary[aria-expanded="true"] .ico-plus{transform:rotate(45deg)}
  /* Both belong to the mobile bars only; declared here so the desktop default
     is "absent" rather than something the media query has to undo. */
  .fsheet-head{display:none}
  .co-bar{display:none}
  /* Sits below the drawers (90/100) but above the shop bar, so the bar keeps
     holding the control that opened the sheet. */
  /* The tint says the view behind is out of reach; the blur says it is not what
     the reader is reading. Both fade together over the same step, so the sheet
     does not arrive over a page that snapped out of focus before it. */
  .sheet-backdrop{
    position:fixed;inset:0;z-index:70;
    background:var(--nu-bg-scrim);
    -webkit-backdrop-filter:blur(0);backdrop-filter:blur(0);
    opacity:0;pointer-events:none;
    transition:opacity var(--nu-motion-base) ease, backdrop-filter var(--nu-motion-base) ease;
  }
  .sheet-backdrop.open{
    opacity:1;pointer-events:auto;
    -webkit-backdrop-filter:blur(var(--nu-blur-md));backdrop-filter:blur(var(--nu-blur-md));
  }

  /* ---------- shop body: filters column + grid ---------- */
  .shop{
    display:grid;
    grid-template-columns:230px 1fr;
    gap:var(--nu-gutter-column);
    padding:0 var(--nu-space-small) var(--nu-space-max);
    align-items:start;
  }
  .shop.filters-hidden{grid-template-columns:0 1fr;gap:0}
  .filters{
    display:flex;flex-direction:column;gap:var(--nu-space-medium);
    /* Stops at the block's own edge, which is where the grid beside it comes out
       from behind it: the two columns start on one line and stay on it. A step
       above the block, so the ramp reaching below it washes the covers going
       past and not the controls standing still - the column is not something the
       reader scrolls past on her way to the books, it is how she decides which
       books. It can sit above the block without ever covering it, having nowhere
       to go: it stops exactly where the block ends. */
    position:sticky;top:var(--nu-masthead-height);z-index:81;
    overflow:hidden;
    opacity:1;
  }
  /* While a packshot is on its way back to its card the column gives up that
     step, so the flight passes in front of it rather than behind. Nothing is
     scrolling at that moment, so there is no ramp to keep clear of. */
  body.is-flying .filters{z-index:auto}
  .shop.filters-hidden .filters{opacity:0;pointer-events:none}
  .filters-ghost{pointer-events:none;z-index:5}
  .filter-row{display:flex;flex-direction:column;gap:var(--nu-space-micro)}
  .filter-label{color:var(--nu-fg-secondary);font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase}
  .chip-row{display:flex;flex-wrap:wrap;gap:var(--nu-space-nano) var(--nu-space-small);align-items:baseline}
  .chip{position:relative;color:var(--nu-fg-primary)}
  /* The underline belongs to the label, not to the whole button: the count is a
     separate piece of information, and running under it would read as part of the
     name. Hence the inner span - a border cannot stop halfway across an element. */
  .chip .chip-t{border-bottom:1px solid transparent;padding-bottom:var(--nu-underline-offset)}
  .chip sup{font:var(--nu-type-caption);color:var(--nu-fg-tertiary);margin-left:var(--nu-space-nano)}
  .chip[aria-pressed="true"] .chip-t{border-bottom-color:var(--nu-border-primary)}
  .chip:hover .chip-t{border-bottom-color:var(--nu-fg-secondary)}
  .chip:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  .chip:disabled{color:var(--nu-fg-tertiary);pointer-events:none}


  /* sort */
  /* The field takes the room the two buttons leave, so the bar keeps its shape
     as their labels change with the language. */
  /* A flex row, not a block: an input is an inline-level box, so in a block
     wrapper it sits on a text baseline and the wrapper reserves descender space
     under it that the field never fills. The wrapper then measures taller than
     its own content and the bar centres the wrong height. */
  .search-wrap{position:relative;display:flex;align-items:center;flex:0 0 auto;min-width:0}
  /* Same construction as the select: the drawing sits inside the field and the
     field leaves it room, so the text never runs under it. Full strength here,
     not the placeholder's grey: on a field without a box the glyph is what says
     a field is there at all. */
  .search-wrap .ico-search{
    position:absolute;left:0;top:50%;
    transform:translateY(-50%);pointer-events:none;color:var(--nu-fg-primary);
  }
  /* Written on the two-class selector on purpose: .input.in-bar sets padding
     with the shorthand, so a padding-left declared at equal weight would be
     wiped by whichever rule the sheet happens to hold last. */
  .search-wrap .input.in-bar{
    width:var(--nu-field-width-search);
    padding-left:calc(var(--nu-icon-sm) + var(--nu-space-micro));
    transition:width var(--nu-motion-base) ease;
  }
  /* The whole control is open, not the field alone: focus moving to the clear
     button beside it stays inside the wrapper, so the field does not collapse
     under the pointer on its way to being emptied. The same reading as the
     phone group in the form, which darkens both of its parts on focus-within. */
  .search-wrap:focus-within .input.in-bar{width:var(--nu-field-width-search-focus)}
  /* Appears only with something to clear, so the field keeps its own room until
     then. The control is the tertiary button, which already carries the colour,
     the answer under the pointer and the focus ring. */
  .search-clear{position:absolute;right:0;top:50%;transform:translateY(-50%)}
  .search-wrap:has(.search-clear:not([hidden])) .input.in-bar{
    padding-right:calc(var(--nu-icon-sm) + var(--nu-space-micro));
  }
  @media (prefers-reduced-motion:reduce){
    .search-wrap .input.in-bar{transition:none}
  }
  /* A search field draws its own clear button in WebKit, in a shape the shop
     uses nowhere else. The field is cleared with Escape instead. */
  .search-wrap .input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
  .sort{position:relative;text-align:right}
  .sort-btn{padding:var(--nu-space-nano) 0}
  /* Both tertiary buttons that open something read their own aria-expanded:
     the state is declared once, on the element, and the stylesheet follows it.
     No parallel class to keep in step. */
  .sort-menu{
    position:absolute;right:0;top:calc(100% + var(--nu-space-micro));z-index:30;
    background:var(--nu-bg-primary);border:1px solid var(--nu-border-neutral);
    /* The panel is as wide as its longest option and no wider: max-content plus
       nowrap on the options means no label ever breaks, and the width re-measures
       itself when the labels swap between EN and PL, where they differ. */
    width:max-content;
    padding:var(--nu-space-micro) 0;display:none;text-align:left;
    box-shadow:0 8px 24px color-mix(in srgb, var(--nu-bg-shadow) 6%, transparent);
  }
  .sort-btn[aria-expanded="true"] + .sort-menu{display:block}
  .sort-menu button{
    display:block;width:100%;text-align:left;padding:var(--nu-space-micro) var(--nu-space-small);
    white-space:nowrap;
  }
  .sort-menu button:hover{background:var(--nu-bg-secondary)}
  /* Inset, because both controls sit inside a box of their own: a positive offset
     would draw the ring outside the stepper's outline and outside the menu panel. */
  .qty button:focus-visible,
  .sort-menu button:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset-inset)}
  /* The mark on the option in force is the underline the chip, the link and the
     ghost button all draw: a border under the label with 1px of air, so every
     underlined thing in the shop sits the same distance from its word. It rides
     on the label rather than the row, which is the full width of the panel. */
  .sort-menu .lbl{border-bottom:1px solid transparent;padding-bottom:var(--nu-underline-offset)}
  .sort-menu button[aria-checked="true"] .lbl{border-bottom-color:var(--nu-border-primary)}

  /* ---------- grid ---------- */
  .grid{
    display:grid;
    grid-template-columns:repeat(3,1fr);
    gap:var(--nu-space-nano);
  }
  .card{position:relative}
  /* first-visit mosaic: cards come into focus, in random order */
  /* ease-out rather than --nu-ease-zoom: the zoom curve is built to put four
     fifths of a journey behind it in the first quarter of the time, which is
     right for a tile crossing the screen and wrong here - it spends the blur
     before the eye has found the card. Spread evenly, the card is seen coming
     into focus rather than seen already sharp. */
  .grid.mosaic .card{opacity:0;animation:cardIn var(--nu-motion-slower) ease-out forwards;animation-delay:var(--d,0ms)}
  @keyframes cardIn{
    from{opacity:0;filter:blur(var(--nu-blur-sm));transform:translateY(var(--nu-space-nano))}
    to{opacity:1;filter:blur(0);transform:none}
  }
  @media (prefers-reduced-motion:reduce){
    .grid.mosaic .card{animation:none;opacity:1}
  }
  /* Fading belongs to the listing, not to the book. In the grid the reader is
     scanning and needs to sift out what cannot be bought at a glance. On the
     product page she came to look at this one cover, so it stays untouched -
     the badge and the disabled button carry the unavailability there. */
  .card.is-out .tile > .cover{opacity:.38;filter:grayscale(.4)}
  .tile{
    background:var(--nu-bg-secondary);
    aspect-ratio:4/5;
    display:flex;align-items:center;justify-content:center;
    position:relative;overflow:hidden;
  }
  /* The badge carries its own look and nothing about where it sits: a corner of
     a packshot is one place it appears, not part of what it is. The tile holding
     it does the positioning, which is what lets a badge also stand in a row. */
  .badge{
    font:var(--nu-type-label);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase;
    padding:var(--nu-space-nano) var(--nu-space-micro);background:var(--nu-bg-primary);border:1px solid var(--nu-border-primary);
  }
  .tile .badge{position:absolute;top:var(--nu-space-milli);left:var(--nu-space-milli);z-index:2}
  .badge.soon{background:var(--nu-bg-inverse);color:var(--nu-fg-inverse)}
  .badge.last{border-color:var(--nu-border-warning);color:var(--nu-fg-warning)}
  .badge.out{border-color:var(--nu-border-muted);color:var(--nu-fg-secondary);background:transparent}
  .badge.award{border-color:var(--nu-border-highlight);color:var(--nu-fg-highlight);background:var(--nu-bg-highlight)}
  .meta{padding:var(--nu-space-micro) var(--nu-space-nano) var(--nu-space-medium)}
  .meta .title{display:block}
  .meta .author{color:var(--nu-fg-secondary)}
  .meta .price{margin-top:var(--nu-space-nano);font-variant-numeric:tabular-nums}
  .card.is-out .meta .price{color:var(--nu-fg-secondary);text-decoration:line-through}
  .empty{
    grid-column:1/-1;padding:var(--nu-space-max) 0;text-align:center;color:var(--nu-fg-secondary);
  }
  .empty .btn-ghost{margin-left:var(--nu-space-micro)}

  /* ---------- covers (pure CSS book jackets) ---------- */
  .cover{
    width:var(--nu-cover-width);max-width:none;aspect-ratio:7/10;
    position:relative;overflow:hidden;
    box-shadow:0 14px 22px -10px color-mix(in srgb, var(--nu-bg-shadow) 35%, transparent), 0 2px 4px color-mix(in srgb, var(--nu-bg-shadow) 12%, transparent);
    transition:transform var(--nu-motion-slow) ease;
    flex-shrink:0;
  }
  .card a, .card .cardlink{display:block}
  .card .cardlink{cursor:pointer}
  .card a:focus-visible, .card .cardlink:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  .card a:hover .cover, .card .cardlink:hover .cover,
  .card a:focus-visible .cover, .card .cardlink:focus-visible .cover{transform:translateY(-4px)}
  @media (prefers-reduced-motion:reduce){
    .card a:hover .cover, .card .cardlink:hover .cover{transform:none}
    .ico-filter .bar-bot,
    .ico-plus{transition:none}
  }
  .cover .pad{position:absolute;inset:0;padding:9% 10%;display:flex;flex-direction:column}

  /* photographic / image covers – box adopts the image's own proportions */
  .cover.cv-img{aspect-ratio:auto;height:auto}
  .cv-img img{
    position:static;width:100%;height:auto;
    display:block;
  }

  /* ---------- product page ---------- */
  .product{padding:0 var(--nu-space-small) var(--nu-space-max)}
  /* The way back stands in a row of its own, the same height as the bar above
     the grid, and centres itself in it the way the filter toggle centres itself
     there. Pinned to the top of the row instead, it sat higher than the control
     it replaces, and the swap between the two views read as the button jumping. */
  /* Standing in a bar, the button takes the bar's height and centring, exactly
     as the filter toggle does. Outside one - the way out of the cart, of the
     checkout - it still carries its own. */
  .back{margin:0 0 var(--nu-space-medium);min-height:var(--nu-bar-height);align-items:center}
  .productbar .back{margin:0}
  .p-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:var(--nu-gutter-column);align-items:start}
  /* the product packshot is the same kind of box as the grid tile, just bigger */
  .p-tile{
    background:var(--nu-bg-secondary);
    /* always fills its column: capping the width here (e.g. by viewport height)
       leaves the packshot stranded on the left of a wide screen */
    aspect-ratio:4/5;width:100%;
    display:flex;align-items:center;justify-content:center;
    position:relative;overflow:hidden;
  }
  .p-tile .badge{top:var(--nu-space-small);left:var(--nu-space-small)}
  /* What the book has to say arrives the way anything arrives in this shop: out
     of blur, rising the last of the way. The rise is the same step everywhere -
     a line of the bookseller's, a card in the grid, a title here - so that one
     distance means one thing: something settling into the place it was always
     going to take. The packshot has just finished its own journey and is already
     sharp, so the words come to meet it rather than having been there all along.
     The button waits out the sentence it belongs to and then does the same, a
     reader deciding to buy being a reader answering what she has just read. */
  .p-info.p-enter{opacity:0;filter:blur(var(--nu-blur-lg));transform:translateY(var(--nu-space-nano))}
  /* The 50ms is a delay, not a duration: a beat that lets the tile land before
     the text starts, too short to be a step on the scale. */
  .p-info.p-enter-in{
    opacity:1;filter:blur(0);transform:none;
    transition:opacity var(--nu-motion-slower) ease .05s,
               filter var(--nu-motion-slower) ease .05s,
               transform var(--nu-motion-slower) ease .05s;
  }
  /* The button takes the weaker step, the one a cover takes. Text can afford to
     be illegible for a moment; a filled shape cannot afford to be a smudge, and
     at the step words use its ground and its shadow spread into one another
     instead of softening. Same reason as the grid: what is a picture rather than
     a sentence stays recognisable on the way in. */
  .p-cta.p-enter-cta{opacity:0;filter:blur(var(--nu-blur-sm));transform:translateY(var(--nu-space-nano))}
  .p-cta.p-enter-cta-in{
    opacity:1;filter:blur(0);transform:none;
    transition:opacity var(--nu-motion-slow) ease var(--nu-motion-slow),
               filter var(--nu-motion-slow) ease var(--nu-motion-slow),
               transform var(--nu-motion-slow) ease var(--nu-motion-slow);
  }
  .fly-tile{overflow:hidden;background:var(--nu-bg-secondary)}
  .fly-tile .badge{display:none}
  .p-info{max-width:480px;padding-top:var(--nu-space-micro)}
  .p-title{font:var(--nu-type-h1);margin-bottom:var(--nu-space-nano)}
  .p-author{color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-medium)}
  /* Size only, not a style: the font: shorthand resets font-variant-numeric,
     so reaching for --nu-type-body-l here would take the tabular figures away.
     Everything else is inherited and already right. The same holds for the
     order total and for the total in the checkout bar. */
  .p-price{font-size:var(--nu-text-size-lg);font-variant-numeric:tabular-nums;margin-bottom:var(--nu-space-medium)}
  .p-price.is-out{color:var(--nu-fg-secondary);text-decoration:line-through}
  .p-desc{margin-bottom:var(--nu-space-small);max-width:42ch}
  .p-quote{
    /* longhand on purpose: a shorthand carrying a var() cannot be read back by
       anything inspecting the sheet, and the docs measure this component */
    margin-bottom:var(--nu-space-medium);max-width:42ch;
    /* the shorthand first, the exception after it: font: resets font-style */
    font:var(--nu-type-body-m);font-style:italic;color:var(--nu-fg-secondary);
    padding-left:var(--nu-space-small);border-left:1px solid var(--nu-border-neutral);
  }
  /* The one place where the shorthand is reached for because of what it clears:
     the quote above is italic and the attribution is not, and font: returns the
     style to roman on its own. */
  .p-quote .q-by{
    display:block;margin-top:var(--nu-space-micro);
    font:var(--nu-type-body-m);letter-spacing:var(--nu-tracking-compact);color:var(--nu-fg-secondary);
  }
  /* a solid em dash reads heavier than letterforms at the same colour,
     so lighten it slightly to match the text optically */
  .p-quote .q-dash{color:var(--nu-fg-secondary);opacity:.72}
  .p-details{border-top:1px solid var(--nu-border-neutral);margin-bottom:var(--nu-space-large)}
  .p-details div{display:flex;justify-content:space-between;gap:var(--nu-space-medium);padding:var(--nu-space-micro) 0;border-bottom:1px solid var(--nu-border-neutral)}
  .p-details dt{color:var(--nu-fg-secondary)}
  .p-details dd{text-align:right}

  /* author drawer */
  .drawer-backdrop{
    position:fixed;inset:0;z-index:90;background:var(--nu-bg-scrim);
    -webkit-backdrop-filter:blur(0);backdrop-filter:blur(0);
    opacity:0;pointer-events:none;
    transition:opacity var(--nu-motion-slow) ease, backdrop-filter var(--nu-motion-slow) ease;
  }
  .drawer-backdrop.open{
    opacity:1;pointer-events:auto;
    -webkit-backdrop-filter:blur(var(--nu-blur-md));backdrop-filter:blur(var(--nu-blur-md));
  }
  .drawer{
    position:fixed;top:0;right:0;bottom:0;z-index:100;
    width:min(400px, 92vw);
    background:var(--nu-bg-primary);border-left:1px solid var(--nu-border-neutral);
    box-shadow:-18px 0 50px color-mix(in srgb, var(--nu-bg-shadow) 8%, transparent);
    transform:translateX(102%);visibility:hidden;
    transition:transform var(--nu-motion-slow) var(--nu-ease-slide), visibility var(--nu-motion-slow);
    padding:var(--nu-space-medium) var(--nu-space-large) var(--nu-space-xlarge);overflow-y:auto;
  }
  .drawer.open{transform:none;visibility:visible}
  @media (prefers-reduced-motion:reduce){
    .drawer,.drawer-backdrop{transition:none}
  }
  .drawer-close{position:absolute;top:var(--nu-space-medium);right:var(--nu-space-medium)}
  .d-ava{
    width:var(--nu-avatar-size-md);height:var(--nu-avatar-size-md);border-radius:50%;
    background:var(--nu-bg-secondary);border:1px solid var(--nu-border-neutral);
    display:inline-flex;align-items:center;justify-content:center;
    font:var(--nu-type-body-l);letter-spacing:var(--nu-tracking-compact);color:var(--nu-fg-primary);
    object-fit:cover;overflow:hidden;margin:var(--nu-space-medium) 0 var(--nu-space-medium);
  }
  .d-label{
    font:var(--nu-type-caption);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase;
    color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-micro);
  }
  .d-name{
    font:var(--nu-type-h2);margin-bottom:var(--nu-space-small);
  }
  .d-bio{font:var(--nu-type-body-m);color:var(--nu-fg-primary);max-width:38ch}
  /* A motif needs two levels the author's drawer does not: the thesis, which
     stands under the name like a subtitle, and the note about where the term
     comes from, which reads as a footnote and is ruled off from the prose. */
  /* Both read at the size of the prose they sit with and step back by colour
     alone: the thesis above it, the note about the term below. One rule, because
     the only thing that separates them is the space under the thesis. */
  .d-lead,.d-note{font:var(--nu-type-body-m);color:var(--nu-fg-secondary);max-width:38ch}
  .d-lead{margin-bottom:var(--nu-space-small)}
  .d-origin{
    margin-top:var(--nu-space-large);padding-top:var(--nu-space-small);
    border-top:1px solid var(--nu-border-neutral);
  }
  /* The motifs are a row of the details list, so the term reads like the genre
     beside it and the answers sit where every other answer sits. The only thing
     the row needs of its own is for the buttons to gather at the right edge and
     wrap there, which is what a details answer does when it runs long. */
  .p-details dd .chip-row{justify-content:flex-end}
  /* The variant lives in one class. .p-cta, .cart-cta and .order-btn stay on the
     markup beside it, but they carry only what depends on where the button sits -
     width, margin, padding, the add-to-cart sequence. Adding a fourth primary
     means writing btn-primary, not extending four selector lists.

     Two heights, and which one a button gets follows from one thing: a button
     standing in text keeps the variant's own, and a button running the full width
     of a column takes one step more, which lands it exactly on the height of a
     field. A form is a column of things one under another, and a button that ends
     one is the same object as the fields above it or it is a mistake. A button
     among words has no such neighbour and should not be that tall.

     That equality is why both solid variants carry a 1px edge in the box whether
     or not it is drawn: a field has a border, so a button that wants a field's
     height has to count one too. Three heights, which is what this was, followed
     from nothing. */
  /* At rest it is one flat colour and nothing else: no gradient, no border, no
     shadow, square corners. The shop around it is quiet, and a button built like
     a physical object was arguing with it. The whole of the treatment now belongs
     to the pointer, so it is a thing the reader brings out rather than a thing
     that was sitting on the page claiming to be lit. */
  .btn-primary{
    position:relative;display:inline-block;padding:var(--nu-space-micro) var(--nu-space-medium);
    color:var(--nu-fg-inverse);letter-spacing:var(--nu-tracking-compact);
    background:var(--nu-bg-action);
    /* Undrawn, but counted - see the note above the variant. */
    border:1px solid transparent;border-radius:0;
    /* Just enough to sit on the page rather than be printed on it: one contact
       shadow, no aura. What the pointer adds is depth on top of this, not the
       first sign that the thing is an object. */
    box-shadow:0 1px 2px color-mix(in srgb, var(--nu-bg-shadow) 10%, transparent);
    transform-origin:center;
    transition:box-shadow var(--nu-motion-quick) ease, transform var(--nu-motion-quick) ease, background var(--nu-motion-base) ease;
  }
  /* Two answers to the pointer, both of them quiet. Under the button, the deep
     burgundy aura and the shallow contact shadow - the two layers of the old
     glass worth keeping. On the button, the whole ground lifting by a tenth of
     the way towards the glow burgundy: evenly, with no shape to it, so it reads
     as the colour waking rather than as anything arriving.

     Nothing inside, in any state. The inner glow this button used to have sat at
     the foot, directly behind the label, and read as a shadow the text was
     casting. */
  .btn-primary:hover{
    background:color-mix(in srgb, var(--nu-bg-action-glow) 10%, var(--nu-bg-action));
    box-shadow:
      0 7px 16px -8px color-mix(in srgb, var(--nu-bg-action-glow-deep) 60%, transparent),
      0 1px 3px color-mix(in srgb, var(--nu-bg-shadow) 18%, transparent);
  }
  /* press: the button shrinks slightly into the page and its shadow tightens,
     so a full-width bar reads the same as a content-width one */
  .btn-primary:active{
    transform:translateY(1px) scale(.985);
    box-shadow:
      0 4px 10px -8px color-mix(in srgb, var(--nu-bg-action-glow-deep) 45%, transparent),
      0 1px 2px color-mix(in srgb, var(--nu-bg-shadow) 12%, transparent);
  }
  /* Built on the primary's terms: one flat colour at rest, nothing inside at any
     point, the whole of the treatment belonging to the pointer.

     It stands one step off the panel it sits on, and the pointer takes it one
     step further in the same direction: away from the grey and into the white on
     a light page, away from the dark on a dark one. The direction carries the
     meaning, not the colour, which is why one sentence covers both schemes.

     On the light side the step is written as a share of the way toward mid grey
     rather than as a colour, because the colour it lands on is not in the
     palette. Whether it should be is a decision about the palette, not about this
     button.

     No outline at rest: standing on the order summary, the button is a shape you
     find rather than one that announces itself. The field beside it holds the
     row. */
  .btn-secondary{
    position:relative;padding:var(--nu-space-micro) var(--nu-space-medium);
    color:var(--nu-fg-primary);letter-spacing:var(--nu-tracking-compact);
    background:var(--nu-bg-action-secondary);
    /* Undrawn, but counted - see the note above the primary. Both solid variants
       carry the 1px so that both land on a field's height. */
    border:1px solid transparent;border-radius:0;
    box-shadow:0 1px 2px color-mix(in srgb, var(--nu-bg-shadow) 8%, transparent);
    transform-origin:center;
    transition:box-shadow var(--nu-motion-quick) ease, background var(--nu-motion-base) ease, transform var(--nu-motion-quick) ease;
  }
  /* Colour and light still answer the pointer; only the 1px press stops being
     eased, so nothing on the page travels. */
  @media (prefers-reduced-motion:reduce){
    .btn-primary,
    .btn-secondary{transition:box-shadow var(--nu-motion-quick) ease, background var(--nu-motion-base) ease}
  }
  /* The same two layers under the button as the primary, in the same places, with
     the colour taken out: this action is not the coloured one and its shadow
     should not be either. */
  .btn-secondary:hover{
    background:var(--nu-bg-action-secondary-lift);
    box-shadow:
      0 7px 16px -8px color-mix(in srgb, var(--nu-bg-shadow) 22%, transparent),
      0 1px 3px color-mix(in srgb, var(--nu-bg-shadow) 14%, transparent);
  }
  .btn-secondary:active{
    transform:translateY(1px) scale(.985);
    box-shadow:
      0 4px 10px -8px color-mix(in srgb, var(--nu-bg-shadow) 16%, transparent),
      0 1px 2px color-mix(in srgb, var(--nu-bg-shadow) 10%, transparent);
  }
  .btn-secondary:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  /* The button stops accepting clicks for the length of the sequence, and taking
     pointer events away takes the hover treatment with them - which is the right
     answer now that the treatment is the hover. The colour stays full strength:
     this is an action that worked, not one that is unavailable. */
  .p-cta.is-adding{pointer-events:none}
  .btn-primary:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  .btn-primary:disabled{
    background:var(--nu-bg-secondary);color:var(--nu-fg-secondary);cursor:not-allowed;
    box-shadow:none;transform:none;
  }
  .cart-wrap{position:relative}
  .cart-n{
    position:absolute;
    top:calc(var(--nu-space-micro) * -1);right:calc(var(--nu-space-micro) * -1);
    min-width:0.9375rem;height:0.9375rem;border-radius:50%;
    background:var(--nu-bg-inverse);color:var(--nu-fg-inverse);font-size:var(--nu-text-size-xs);
    /* A length rather than a ratio: it equals the circle's own height, which is
       what centres the count in it. Both are in rem, so the circle and the digit
       inside it grow together when the reader enlarges text. */
    line-height:0.9375rem;
    text-align:center;padding:0 var(--nu-space-nano);
  }

  /* ---------- cart & checkout ---------- */
  .cart-n.dot-in{animation:dotIn var(--nu-motion-base) ease-out}
  @keyframes dotIn{from{opacity:0;transform:scale(.55)}to{opacity:1;transform:scale(1)}}
  .cart-n.bump{animation:softBump var(--nu-motion-slow) ease}
  @keyframes softBump{0%{transform:scale(1)}50%{transform:scale(1.12)}100%{transform:scale(1)}}
  /* A row, so the confirmation glyph can stand beside the word - and block-level,
     not inline-flex. An inline-flex box takes its baseline from its first item,
     and an SVG has none, so the browser synthesises one at the glyph's bottom
     edge and then reserves the font's descender space below it. The button grew
     taller the moment the icon appeared. Out of the inline flow there is no line
     box to sit in, so the height is the row's height and nothing else. */
  .p-cta .cta-label{
    display:flex;align-items:center;justify-content:center;gap:var(--nu-space-micro);
    transition:opacity var(--nu-motion-quick) ease, transform var(--nu-motion-quick) ease;
  }
  .p-cta.swap-out .cta-label{opacity:0;transform:translateY(-6px)}
  .p-cta.swap-in .cta-label{opacity:0;transform:translateY(6px)}
  @media (prefers-reduced-motion:reduce){
    .p-cta .cta-label{transition:none}
  }

  .cart-drawer h2{font:var(--nu-type-h2);margin:var(--nu-space-nano) 0 var(--nu-space-medium)}
  .ci{
    display:flex;gap:var(--nu-space-small);align-items:flex-start;
    padding:var(--nu-space-small) 0;border-bottom:1px solid var(--nu-border-neutral);
    transition:opacity var(--nu-motion-base) ease, transform var(--nu-motion-base) ease;
  }
  .ci.removing{opacity:0;transform:translateX(14px)}
  /* The row still leaves, it just stops travelling on the way out. */
  @media (prefers-reduced-motion:reduce){
    .ci{transition:none}
  }
  .ci-cover{width:var(--nu-thumb-size-sm);flex-shrink:0}
  /* A cover standing at thumbnail size takes a thumbnail's shadow. The packshot
     casts 14px down with 22px of blur, which is a shelf's worth of depth under a
     44px picture: it swallows the gap beneath and crowds whatever stands there.
     Two places show a cover this small - a cart line and the bookseller's
     proposal - and they share one rule so the two cannot drift. */
  .ci-cover .cover,
  .bs-b-cover .cover{width:100%;max-width:none;box-shadow:0 2px 8px color-mix(in srgb, var(--nu-bg-shadow) 18%, transparent)}
  .ci-main{flex:1;min-width:0}
  .ci-title{display:block;line-height:var(--nu-line-snug);margin-bottom:var(--nu-space-nano)}
  .ci-price{color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-micro)}
  .ci-row{display:flex;align-items:center;justify-content:space-between;gap:var(--nu-space-milli)}
  .qty{display:inline-flex;align-items:center;border:1px solid var(--nu-border-neutral)}
  .qty button{
    width:var(--nu-control-size-sm);height:var(--nu-control-size-sm);
    display:inline-flex;align-items:center;justify-content:center;
    color:var(--nu-fg-primary);transition:background var(--nu-motion-quick) ease;
  }
  .qty button:hover{background:var(--nu-bg-secondary)}
  /* At one the minus has nothing to take away: removing the line is what the
     Remove button is for, and doing it silently from here would be a different
     action wearing the same face. */
  .qty button:disabled{color:var(--nu-fg-tertiary);pointer-events:none}
  .qty span{min-width:var(--nu-control-size-sm);text-align:center;font-variant-numeric:tabular-nums}
  .ci-line{font-variant-numeric:tabular-nums}
  .ci-remove{margin-top:var(--nu-space-micro);align-self:flex-start}
  .cart-empty{color:var(--nu-fg-secondary);padding:var(--nu-space-large) 0}
  .cart-foot{padding-top:var(--nu-space-medium)}
  .fs-note{color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-micro)}
  .fs-bar{height:2px;background:var(--nu-border-neutral);margin-bottom:var(--nu-space-medium);overflow:hidden}
  .fs-fill{height:100%;background:var(--nu-fg-primary);transition:width var(--nu-motion-slower) ease}
  /* The bar shows the new length instead of growing into it. */
  @media (prefers-reduced-motion:reduce){
    .fs-fill{transition:none}
  }
  .sum-row{display:flex;justify-content:space-between;padding:var(--nu-space-nano) 0;font-variant-numeric:tabular-nums}
  .sum-row.muted{color:var(--nu-fg-secondary)}
  /* size only: a style here would reset the tabular figures set by .sum-row */
  /* Tabular figures restated after the style: .sum-row sets them, but font: on a
     more specific selector clears every font property it does not name, and this
     is a column of numbers that has to hold its alignment. */
  .sum-row.total{
    font:var(--nu-type-body-l);font-variant-numeric:tabular-nums;
    padding-top:var(--nu-space-milli);margin-top:var(--nu-space-micro);border-top:1px solid var(--nu-border-neutral);
  }
  .cart-cta{display:block;width:100%;text-align:center;margin-top:var(--nu-space-medium);padding:var(--nu-space-milli) 0}
  .cart-page-list .ci-cover{width:var(--nu-thumb-size-md)}
  .cart-page-list .ci{padding:var(--nu-space-medium) 0}
  .disc-row{display:flex;gap:var(--nu-space-micro);margin-bottom:var(--nu-space-milli)}
  /* The discount code is a text field with two things of its own: it grows into
     the row and it reads in capitals. */
  .disc-row .input{flex:1;text-transform:uppercase;letter-spacing:var(--nu-tracking-caps)}
  /* the code itself is uppercased as you type, but the hint stays sentence case -
     and drops the tracking with it, which belongs to capitals */
  .disc-row .input::placeholder{
    text-transform:none;letter-spacing:var(--nu-tracking-body);
  }
  .disc-applied{margin-bottom:var(--nu-space-milli)}
  .disc-applied .btn-ghost{margin-left:var(--nu-space-micro)}

  /* Cart and checkout are reading-and-deciding views, not a gallery: the whole
     view scales with the window up to a comfortable measure, then stops and
     stays centred, so a shopping row never stretches across a huge screen.
     Centring the view (not the grid) keeps the back link and title on the
     same left edge as the columns below them. */
  .checkout{
    padding:0 var(--nu-space-small) var(--nu-space-max);animation:fadeUp var(--nu-motion-slow) ease;
    max-width:var(--nu-form-max-width);margin-inline:auto;
  }
  .done-view{padding:var(--nu-space-max) var(--nu-space-small);text-align:center;animation:fadeUp var(--nu-motion-slow) ease}
  @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
  @media (prefers-reduced-motion:reduce){
    .checkout,.done-view{animation:none}
    .cart-n.bump,.cart-n.dot-in{animation:none}
  }
  /* Cart and checkout are reading-and-deciding views, not a gallery: they scale
     with the window up to a comfortable measure, then stop and stay centred
     instead of stretching a shopping row across a very wide screen. */
  .co-grid{display:grid;grid-template-columns:1.9fr 1fr;gap:var(--nu-gutter-column);align-items:start}
  .co-h{font:var(--nu-type-h2);margin:var(--nu-space-micro) 0 var(--nu-space-medium)}
  .co-sec{margin-bottom:var(--nu-space-large)}
  .co-sec h3{
    font:var(--nu-type-label);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase;
    color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-small);
  }
  .f-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--nu-space-milli)}
  .f-grid .wide{grid-column:1/-1}
  /* Street, building and flat are one address, so they share a row. The row
     declares three tracks - label, control, message - and each field hands its
     own three rows up to it through subgrid. That is what keeps the controls in
     line: a label that wraps to two lines, or a message that appears under one
     field, moves that row for the whole group instead of shifting one field
     against its neighbours. Street takes what is left, the numbers take the
     width they need. */
  .f-addr{
    grid-column:1/-1;display:grid;align-content:start;
    grid-template-columns:1fr var(--nu-field-width-short) var(--nu-field-width-short);
    grid-template-rows:auto auto auto;gap:0 var(--nu-space-milli);
  }
  .f-addr .field{display:grid;grid-template-rows:subgrid;grid-row:span 3}
  /* The label row is as tall as the tallest label, so a short label would float
     above its control while a two-line one sat right on top of its own. Every
     label goes to the bottom of that row instead, and each one keeps the same
     distance from the control it names. */
  .f-addr .field label{align-self:end}
  /* Two components, not one built out of the other. They declare the same box
     twice on purpose: a shared selector list is what made a select impossible to
     place outside a form field, and made a group of two controls undo somebody
     else's rule. */

  /* Text field. */
  .input{
    width:100%;box-sizing:border-box;
    border:1px solid var(--nu-border-neutral);border-radius:0;
    padding:var(--nu-space-milli);font:inherit;line-height:var(--nu-line-normal);
    background:var(--nu-bg-primary);transition:border-color var(--nu-motion-base) ease;
  }
  .input:focus{outline:none;border-color:var(--nu-border-primary)}
  .input::placeholder{color:var(--nu-fg-tertiary)}
  .input.is-error{border-color:var(--nu-border-alert)}
  /* A field standing in a bar rather than in a form. The bar is a row of
     typographic controls - a label with an icon, a label with a menu - so a box
     among them reads as a form dropped into a toolbar. The frame comes down to
     the mark this shop already uses everywhere else: one rule under the words,
     at the distance every other underline keeps.

     The focus ring comes back here. It was taken off the boxed field because an
     outline drawn inside a border reads as a second border; without the border
     that reason is gone, and a rule alone is too quiet to carry focus on its own. */
  /* The rule under this field is not an underline the way the ghost button's is.
     It is the field's frame, kept on one side and dropped on the other three -
     so the distance from the value to it is the distance the boxed field keeps
     from its own border, and comes from the component rather than from whatever
     stands next to it. The bar centres boxes, and the value sits in the middle
     of its own, so the three labels land on one line regardless of height. */
  /* The same treatment as the strapline, and for the same reason: an idle search
     is a prompt and a hairline, and both were disappearing into whatever cover
     happened to be behind them. It lasts exactly as long as the field is idle -
     taking the focus or typing a word puts it back on the page's own colours,
     where a query has to read as text and not as an effect. The magnifier goes
     with them, being part of the same prompt. */
  .input.in-bar{
    border:0;border-bottom:1px solid var(--nu-border-neutral);
    padding:var(--nu-space-milli) 0;
    background:transparent;
  }
  /* Focus is shown by the field's own rule rather than by the shared ring: an
     outline drawn around a field with no box reads as a box appearing, louder
     than the control it marks. The rule goes to full strength instead - the same
     move the tertiary button and the link make, in the other direction.

     The two values are the ones the ghost button already uses for the one
     permanent underline in the shop: neutral at rest, primary when acted on. The
     pressed chip makes the same move from transparent. So the field is not built
     to resemble those marks, it is built from them.

     This is a colour change, and it is allowed to be: 1.4.1 governs telling one
     element from another, focus has its own criterion. The changed area is the
     rule, 1px by the field's width, against a 4px line along its shortest side;
     the two states stand 18:1 apart, where three is asked for. And it is a
     change in lightness, not in hue, so it does not depend on colour vision. */
  .input.in-bar:focus{outline:none}
  .search-wrap:focus-within .input.in-bar{border-bottom-color:var(--nu-border-primary)}
  .input.in-bar{transition:border-color var(--nu-motion-quick) ease}

  /* Select. Native selects render taller or shorter than a text field and park
     their arrow on the edge, so the native look is dropped and the chevron is
     drawn as an icon, which takes the field's own colour through currentColor.
     The right padding is the room that chevron stands in. */
  .select{
    width:100%;box-sizing:border-box;
    border:1px solid var(--nu-border-neutral);border-radius:0;
    padding:var(--nu-space-milli);
    /* the room the chevron stands in: the field's own padding, the icon, and the
       gap between the value and it */
    padding-right:calc(var(--nu-space-milli) + var(--nu-icon-sm) + var(--nu-space-medium));
    font:inherit;line-height:var(--nu-line-normal);
    background:var(--nu-bg-primary);transition:border-color var(--nu-motion-base) ease;
    appearance:none;-webkit-appearance:none;-moz-appearance:none;
  }
  .select::-ms-expand{display:none}
  .select:focus{outline:none;border-color:var(--nu-border-primary)}
  .select-wrap{position:relative;display:block}
  .select-wrap .ico-chevron{
    position:absolute;right:var(--nu-space-milli);top:50%;
    transform:translateY(-50%);pointer-events:none;
  }

  /* Two controls that are one answer stand side by side without a gap; the left
     one gives up its right border, so the seam between them is a single line.
     Focus and error take the whole group, otherwise the outline would change
     colour halfway along its top edge. */
  .f-group{display:grid;grid-template-columns:var(--nu-field-width-short) 1fr}
  .f-group .select{border-right:0}
  .f-group:focus-within .select,
  .f-group:focus-within .input{border-color:var(--nu-border-primary)}
  .f-group:has(.is-error) .select{border-color:var(--nu-border-alert)}

  .field label{display:block;font:var(--nu-type-body-s);color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-nano)}
  .f-hint{text-transform:none;letter-spacing:var(--nu-tracking-body);color:var(--nu-fg-tertiary)}
  /* A line that stays empty until something is wrong. It pushes the fields below
     it rather than reserving space in every one: eight fields holding a blank
     line each would cost more than the shift costs when one is wrong. */
  .field-msg{
    font:var(--nu-type-body-s);color:var(--nu-fg-alert);
    margin-top:var(--nu-space-nano);
  }
  .field-msg:empty{display:none}
  /* A checkbox draws no border of its own, so the alert is an outline. */
  .consent input.is-error{outline:var(--nu-focus-ring) solid var(--nu-border-alert);outline-offset:var(--nu-focus-offset)}
  .opt{
    display:flex;align-items:center;gap:var(--nu-space-milli);
    border:1px solid var(--nu-border-neutral);padding:var(--nu-space-milli) var(--nu-space-small);margin-bottom:var(--nu-space-micro);cursor:pointer;
    transition:border-color var(--nu-motion-base) ease;
  }
  .opt:hover{border-color:var(--nu-border-hover)}
  .opt:has(input:checked){border-color:var(--nu-border-primary)}
  .opt input{accent-color:var(--nu-fg-primary);margin:0}
  .opt .o-main{flex:1}
  .opt .o-sub{font:var(--nu-type-body-s);color:var(--nu-fg-secondary)}
  .opt .o-price{font-variant-numeric:tabular-nums}
  .consent{display:flex;gap:var(--nu-space-milli);align-items:flex-start;margin-bottom:var(--nu-space-milli);cursor:pointer}
  .consent input{accent-color:var(--nu-fg-primary);margin-top:var(--nu-space-nano);flex-shrink:0}
  /* A native checkbox and a native radio draw the browser's own ring, of a
     thickness and a colour nothing else in the shop uses. They take the shop's
     ring instead: the field around them keeps its border either way, so there
     is no second border to read, and the mark stays the same everywhere. */
  .opt input:focus-visible,.consent input:focus-visible{
    outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  .legal{font:var(--nu-type-body-m);color:var(--nu-fg-secondary);margin-top:var(--nu-space-medium);max-width:60ch}
  .co-side{position:sticky;top:var(--nu-space-medium);background:var(--nu-bg-secondary);padding:var(--nu-space-medium)}
  .co-side h3{font:var(--nu-type-label);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase;color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-small)}
  .co-item{display:flex;justify-content:space-between;gap:var(--nu-space-small);padding:var(--nu-space-nano) 0}
  .co-item .q{color:var(--nu-fg-secondary)}
  .order-btn{display:block;width:100%;margin-top:var(--nu-space-medium);padding:var(--nu-space-milli) 0;text-align:center}
  .done-no{font:var(--nu-type-h1);margin:var(--nu-space-small) 0 var(--nu-space-medium)}
  .done-view p{max-width:46ch;margin:0 auto var(--nu-space-milli);color:var(--nu-fg-secondary)}
  .done-back{display:inline-block;margin-top:var(--nu-space-medium)}

  /* ---------- the bookseller ----------
     A corner of the page rather than a layer over it. She sits above the sort
     menu and the checkout bar, and below the filter sheet, the shop bar and both
     drawers: everything the reader opened on purpose outranks something that
     spoke without being asked. */
  .bs{
    /* The corner belongs to the reader while the page is under it, and to the
       page once the footer arrives. --bs-lift is how much of the footer is on
       screen, measured on scroll: at the bottom of a view she stands on the last
       line of content instead of on the colophon. */
    /* Named, because the panel above has to subtract it to work out how tall it
       may grow, and the value differs between the wide window and the phone. */
    --bs-bottom:calc(var(--nu-space-medium) + var(--bs-lift, 0px));
    /* What she must not grow into at the top: the block that stays there, and a
       step of air under it. Named, because the panel's ceiling is one decision
       and the value differs between the wide window and the phone, where nothing
       is pinned to the top at all. */
    --bs-headroom:calc(var(--nu-masthead-height) + var(--nu-space-medium));
    position:fixed;right:var(--nu-space-medium);z-index:75;
    bottom:var(--bs-bottom);
    /* The mark keeps the corner and the offer opens upward from it, the way a
       speech bubble stands over whoever is speaking. Beside it, the panel pushed
       the mark off the corner every time it appeared. */
    display:flex;flex-direction:column;align-items:flex-end;gap:var(--nu-space-micro);
  }
  /* Room for the panel to be as wide as it needs and no wider. A measure, not a
     spacing step: this is how long a line may run before it stops being easy to
     read, which the scale has nothing to say about. */
  /* One width, because the panel always holds the same thing: two sentences and
     the proposals they name, with covers, titles, prices and prose, all of which
     want room. The panel is anchored to the right, so it grows leftward into the
     page rather than pushing the mark. */
  .bs-panel{
    position:relative;
    max-width:46ch;
    /* It never grows past what is left for it. Anchored at the bottom, a panel
       taller than that climbs out through the top edge, takes its own close
       button with it, and slides behind whatever is pinned up there. What is
       left over after the mark, the gap under it and the headroom is all it may
       have; the list of proposals takes the overflow and scrolls, so the offer
       and the way out stay put.

       dvh rather than vh on a phone, where the address bar is part of the height
       and vh is the tallest the window ever gets rather than the current one. */
    max-height:calc(100vh - var(--bs-bottom) - var(--nu-space-micro) - var(--nu-avatar-size-sm) - var(--bs-headroom));
    max-height:calc(100dvh - var(--bs-bottom) - var(--nu-space-micro) - var(--nu-avatar-size-sm) - var(--bs-headroom));
    display:flex;flex-direction:column;min-height:0;
    background:var(--nu-bg-primary);
    border:1px solid var(--nu-border-neutral);
    /* One step on all four sides. The panel row of the spacing table asks for
       --nu-space-medium, and that belongs to a drawer or an order summary; this
       one is a third of a drawer's width and the same air would leave too narrow
       a measure. The sort menu already sets the precedent for a floating panel
       taking less than a docked one. */
    padding:var(--nu-space-small);
    box-shadow:0 8px 24px color-mix(in srgb, var(--nu-bg-shadow) 8%, transparent);
    animation:bsIn var(--nu-motion-slow) var(--nu-ease-slide);
  }
  @keyframes bsIn{from{opacity:0;transform:translateY(var(--nu-space-micro))}to{opacity:1;transform:none}}
  /* An answer opening changes how much room the panel needs, in both directions
     at once, and arriving at the new size in one frame reads as a second panel
     replacing the first. It walks there instead: the script pins the size it had
     and sets the one it needs, and this is what carries it between the two. The
     clip is for the moment in between, while the content is larger than the box
     on its way to fitting it. */
  .bs-panel.is-resizing{
    overflow:hidden;
    transition:width var(--nu-motion-base) var(--nu-ease-slide),
               height var(--nu-motion-base) var(--nu-ease-slide);
  }
  /* She arrives without travelling for a reader who asked for less motion: the
     panel still appears, it just stops sliding. */
  @media (prefers-reduced-motion:reduce){
    .bs-panel{animation:none}
  }
  /* The close sits where it does on both drawers, so the way out of anything
     that opened over the page is always in the same corner of it. */
  .bs-close{
    position:absolute;top:var(--nu-space-micro);right:var(--nu-space-micro);
  }
  /* Room for the close, so a two-line offer never runs under it. */
  .bs-say{padding-right:var(--nu-space-large)}
  /* A button is centred by the browser and a flex child is stretched by default,
     and the two together put the label in the middle of a panel-wide control.
     The ghost here shrinks to its own words and starts at the left edge, the
     way it does everywhere else in the shop. */
  .bs-why{align-self:flex-start;text-align:left}
  /* The one part that gives way. Everything above it is the offer and the way
     out, and both have to stay where the reader last saw them. */
  .bs-list{margin-top:var(--nu-space-small);overflow-y:auto;min-height:0}
  /* One proposal: the cover, the title and what it costs. The same three things
     a cart line shows, at the same thumbnail size, because a reader deciding
     between two books wants exactly what she wants when deciding to keep one. */
  /* One proposal is three things - the link, the reason and the reason's text -
     and they are one block, not three siblings in a row. The wrapper carries the
     rule and the air, so the last control in a proposal cannot end up leaning on
     the next proposal's divider. */
  .bs-item{
    padding:var(--nu-space-small) 0;border-top:1px solid var(--nu-border-neutral);
  }
  /* The last block has nothing under it but the panel's own edge, and that edge
     already keeps a step. The same rule the toggle above follows: a control's air
     underneath belongs to whatever it stands above, never to two things at once. */
  .bs-item:last-child{padding-bottom:0}
  /* The cover and the title lead to the book, so they are one link: something
     that goes to an address is an <a>, and the reader gets a target the size of
     the thumbnail rather than the size of a word. */
  .bs-b-go{display:flex;gap:var(--nu-space-micro);align-items:flex-start;flex:1;min-width:0}
  .bs-b-go:hover .bs-b-title{border-bottom-color:var(--nu-border-primary)}
  .bs-b-go:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  /* The same step above as the block keeps below it, and no correction: the
     thumbnail's shadow reaches ten pixels, which sits comfortably inside this
     gap. It needed one while the cover was still carrying the packshot's shadow
     and throwing twenty-six pixels into an eight-pixel space. No margin below -
     the block's own padding is what stands between this control and the next
     hairline. */
  .bs-why{margin:var(--nu-space-small) 0 0}
  .bs-b-cover{width:var(--nu-thumb-size-sm);flex-shrink:0}
  .bs-b-main{flex:1;min-width:0}
  /* The title carries the underline, not the row: the mark belongs to the words
     the way it does on a ghost button, and the price underneath is not part of
     what is being named. */
  .bs-b-title{
    display:inline;border-bottom:1px solid var(--nu-border-neutral);
    padding-bottom:var(--nu-underline-offset);
    transition:border-color var(--nu-motion-quick) ease;
  }
  .bs-b-meta{display:block;margin-top:var(--nu-space-nano);color:var(--nu-fg-secondary);font-variant-numeric:tabular-nums}
  /* What she says about one book, once it has been chosen. Its own scroll,
     because this is the one part whose length the shop does not control. */
  /* A margin, not padding, and it stays one whatever else changes here: padding
     would sit inside a scrolling area and travel with the text, putting the first
     line against the button the moment anything moved.

     No scroll of its own. The list above is the one place that scrolls, because
     two scrolling areas one inside the other in a panel this narrow leaves the
     reader guessing which one her wheel is turning.

     No air underneath: the block around it already ends the proposal, and two
     spacings meaning the same thing add up to one that reads as a mistake. */
  .bs-text{
    font:var(--nu-type-body-m);color:var(--nu-fg-secondary);
    margin-top:var(--nu-space-micro);
  }
  .bs-text p{margin-bottom:var(--nu-space-micro)}
  .bs-text > :last-child{margin-bottom:0}
  .bs-text ul{margin:var(--nu-space-micro) 0;padding-left:var(--nu-space-small)}
  .bs-text li{margin-bottom:var(--nu-space-micro)}
  .bs-text strong{color:var(--nu-fg-primary)}
  /* The mark. The wordmark's accent at rest: it stands for the shop speaking,
     so it borrows the shop's own sign rather than inventing a face for someone
     who deliberately has no name. Still, because the one in the header is the
     one that moves and two of them pulsing would compete. */
  /* The mark stays on the page after the panel is dismissed, and pressing it
     brings the offer back. Closing an offer is not the same as refusing the
     bookseller, so the second act needs a way back that the first one does not
     take away. */
  .bs-ava{
    width:var(--nu-avatar-size-sm);height:var(--nu-avatar-size-sm);flex-shrink:0;
    border-radius:50%;background:var(--nu-bg-secondary);border:1px solid var(--nu-border-neutral);
    display:inline-flex;align-items:center;justify-content:center;
  }
  .bs-ava:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  /* Its own pair of names rather than the wordmark's, carrying the wordmark's
     pair of values: the mark looks drawn from the logotype's colour, and a
     change to the logo does not reach the corner afterwards.

     Plain colours rather than light-dark() for the same reason the logotype
     splits its own: these are read inside @keyframes, and a keyframe resolves
     light-dark() once, against a scheme that is not the one in force. */
  .bs-dot{
    width:var(--nu-space-micro);height:var(--nu-space-micro);
    border-radius:50%;background:var(--nu-bg-ava-dot);
    position:relative;
  }
  /* The mark answers the pointer with light rather than with a border, because
     the mark is a light and every other control in the shop is a box. Half the
     strength of a breath and held for as long as the pointer stays: at rest it
     is a dot, under the pointer it is lit and waiting, working it breathes. One
     signal at three levels rather than three signals.

     It lights the way a breath does, and takes as long: --nu-motion-slower on
     --nu-ease-zoom is the rising half of bsBloom to within ten milliseconds. At
     the hover step the same light arrived in a tenth of the time, which read as
     switched on rather than as lit.

     The lit state is a layer of its own and the only thing that transitions is
     its opacity. The dot's resting colour changes with the scheme, and a colour
     under transition is a colour on its way somewhere: putting the two together
     left the mark finishing a journey to the value the page had just stopped
     using. Nothing about the scheme is animated now - it lands, and the layer
     above it fades.

     Both of these stay under prefers-reduced-motion, on the rule the shop
     already follows: colour and shadow answer the pointer instead of moving the
     page.

     The light is drawn once, at full strength, in two layers over the resting
     dot: the core and the halo. Everything after that is how far each layer is
     turned up. Nothing at rest, all of it at the peak of a breath, and half of
     it while the pointer waits - so "ready is half a breath" is the number .5
     and not two sets of matching figures that happen to agree today.

     Two layers rather than one because the two are not turned up together: a
     lit core at half would be grey, and the mark is either lit or it is not. It
     is the halo that comes in at half. */
  .bs-dot::before,
  .bs-dot::after{
    content:"";position:absolute;inset:0;border-radius:50%;
    opacity:0;
    /* Going out: the breath's release. */
    transition:opacity var(--nu-motion-slower) ease-in-out;
  }
  /* The core, lit. */
  .bs-dot::before{background:var(--nu-bg-ava-dot-bloom)}
  /* The halo, at the strength a breath reaches. Five rings from the core
     outwards, the last one reaching the rim of the avatar, so the light stops
     where the ground does.

     A box-shadow and not a filter, on the logotype's reasoning: a filter
     rasterises the element it runs on, and at eight pixels across that shows as
     a ragged edge. A shadow on a border-radius circle draws the same halo and
     stays vector. It needs no background of its own - a shadow is cast from the
     box, not from what fills it. */
  .bs-dot::after{
    box-shadow:0 0 5px  color-mix(in srgb, var(--nu-bg-glow-warm) 85%, transparent),
               0 0 10px color-mix(in srgb, var(--nu-bg-glow-bright) 80%, transparent),
               0 0 15px color-mix(in srgb, var(--nu-bg-glow-fresh) 80%, transparent),
               0 0 20px color-mix(in srgb, var(--nu-bg-glow-cool) 80%, transparent),
               0 0 26px color-mix(in srgb, var(--nu-bg-glow-dusk) 80%, transparent);
  }
  /* Coming in: the breath's rise, on both layers. */
  .bs-ava:hover .bs-dot::before,
  .bs-ava:hover .bs-dot::after{
    transition:opacity var(--nu-motion-slower) var(--nu-ease-zoom);
  }
  .bs-ava:hover .bs-dot::before{opacity:1}
  .bs-ava:hover .bs-dot::after{opacity:.5}
  /* Light rather than size. The mark answers by brightening the way the logotype
     accent does - the core goes to its bloom colour and the halo opens out until
     it fills the ground behind it - so the one thing that glows in the shop and
     the one thing that glows in the corner read as the same gesture.

     One shape for both states, and the whole difference between them is how many
     times it runs. Something has happened: one breath. Something is still
     happening: breathing. A separate shape for each would have made them two
     signals to learn instead of one, and the mark has nothing else to say.

     --nu-motion-hold is one breath, which is what that step already measures:
     the rate a person breathes when thinking rather than hurrying.

     Taken in, then let out: --nu-ease-zoom on the way up, so the light is
     already there within about a tenth of a second of the press, and the long
     ease-in-out release afterwards. A symmetrical breath was the first shape and
     the wrong one - the answer to a click has to arrive inside the moment the
     click is still being felt, and a slow opening put a beat of nothing between
     the two, which read as the mark responding to something else.

     The breath turns the same two layers up and down, so it names no colour and
     no radius. Everything the mark can look like is declared above, once. */
  @keyframes bsBreath{
    0%  {opacity:0; animation-timing-function:var(--nu-ease-zoom)}
    30% {opacity:1; animation-timing-function:ease-in-out}
    100%{opacity:0}
  }
  /* Once, whenever something in the panel opens - the only moment the shop is
     putting an answer together. The curve comes from the keyframes themselves,
     so the shorthand sets none. */
  .bs-dot.is-thinking::before,
  .bs-dot.is-thinking::after{animation:bsBreath var(--nu-motion-hold)}
  /* Without end: from the moment an answer is asked for until its last word has
     arrived. Waiting for the words and setting them down are one stretch of work
     as far as the reader is concerned, and the mark stops when the work does. */
  .bs-dot.is-working::before,
  .bs-dot.is-working::after{animation:bsBreath var(--nu-motion-hold) infinite}

  /* Where the answer will be. Three rules of falling length, so the shape reads
     as a paragraph rather than as a control, and no text at all: a placeholder
     that says words would be a sentence nobody wrote. Marked aria-hidden and
     paired with aria-busy on the box, so assistive technology is told the region
     is being filled instead of being read three empty bars. */
  .bs-sk{display:block}
  .bs-sk span{
    display:block;height:var(--nu-space-micro);
    background:var(--nu-bg-secondary);
    margin-bottom:var(--nu-space-micro);
    animation:bsPend var(--nu-motion-slower) ease-in-out infinite alternate;
  }
  .bs-sk span:nth-child(1){width:100%}
  .bs-sk span:nth-child(2){width:86%}
  .bs-sk span:nth-child(3){width:62%;margin-bottom:0}
  @keyframes bsPend{from{opacity:1}to{opacity:.45}}

  /* A line of text comes out of blur as it fades in and settles the last of the
     way up, rather than simply appearing. The blur is what makes the arrival
     readable as writing rather than as switching on: opacity alone changes only
     how dark the words are. The unit is the line, not the word - a line is what
     the eye takes in at once, so it is what arrives at once.

     Transparent rather than absent. Every word holds its place from the moment
     the answer is written, so the panel reaches its full height at once and the
     words arrive into room that is already theirs. The space under the text
     while it comes in is the point: a box creeping open a line at a time reads
     as the panel doing the work, and the work belongs to the words.

     It also means the lines the browser settled on are there to be measured, and
     that nothing reflows while the passage arrives.

     A line is still arriving when the next one sets off, which is what makes the
     passage read as one thing coming in rather than as a stack of separate
     arrivals. inline-block because the rise is a transform, and a transform does
     nothing to a plain inline box. */
  .bs-w{display:inline-block;opacity:0}
  .bs-w.is-in{animation:bsLineIn var(--nu-motion-slower) var(--nu-ease-zoom) both}
  /* A list marker is drawn by the item, not by the words, so it stood there in
     full while the words it belongs to were still on their way. The item draws
     its own instead, which can wait and then arrive on the same terms as the
     first line of the item - the line whose words turn the selector true. It
     sits in the room the list already leaves for a marker. */
  .bs-text li{list-style:none;position:relative}
  .bs-text li::before{
    content:"\2022";position:absolute;left:calc(-1 * var(--nu-space-small));
    color:var(--nu-fg-tertiary);opacity:0;
  }
  .bs-text li:has(.bs-w.is-in)::before{animation:bsLineIn var(--nu-motion-slower) var(--nu-ease-zoom) both}
  /* An answer given once already is put down whole, without words to wait for,
     and then there is nothing for the marker to wait for either. */
  .bs-text li:not(:has(.bs-w))::before{opacity:1}
  @keyframes bsLineIn{
    from{opacity:0;filter:blur(var(--nu-blur-lg));transform:translateY(var(--nu-space-nano))}
    to{opacity:1;filter:blur(0);transform:none}
  }

  @media (prefers-reduced-motion:reduce){
    .bs-dot.is-thinking::before,.bs-dot.is-thinking::after,
    .bs-dot.is-working::before,.bs-dot.is-working::after,
    .bs-sk span{animation:none}
    .bs-w,.bs-w.is-in{opacity:1;animation:none}
    .bs-text li::before,.bs-text li:has(.bs-w.is-in)::before{opacity:1;animation:none}
    .bs-panel.is-resizing{transition:none}
  }
  @media (max-width:820px){
    /* The corner is where the shop bar lives on a phone, so she sits on top of
       it rather than over it, and spans the width instead of floating in a
       corner there is no room for. */
    /* The corner is where the shop bar lives on a phone, but only on the views
       that have one. The product page has none, so the clearance is asked for by
       the same class that reserves room for the bar everywhere else, instead of
       being assumed - a mark hovering above an absent bar reads as misplaced. */
    .bs{
      left:var(--nu-space-micro);right:var(--nu-space-micro);
      --bs-bottom:calc(var(--nu-space-micro) + var(--bs-lift, 0px));
      /* Nothing is pinned to the top here, so the ceiling is the air alone. */
      --bs-headroom:var(--nu-space-medium);
    }
    body.has-mobar .bs{
      --bs-bottom:calc(var(--nu-mobar-height) + var(--nu-space-micro) + var(--bs-lift, 0px));
    }
    /* Narrower here than the room allows. Edge to edge, an offer stops reading
       as something said in the corner and starts reading as a banner across the
       page. The measure is shorter than on a wide window, and the container's
       own edges cap it on a narrow phone.

       The panel keeps to the right, above the mark, and everything inside it
       stays flush left, exactly as on a wide window. */
    .bs-panel{max-width:34ch;min-width:0}
  }

  /* ---------- site footer ---------- */
  .site-foot{
    display:flex;align-items:center;justify-content:space-between;gap:var(--nu-space-medium);
    padding:var(--nu-space-medium) var(--nu-space-small) var(--nu-space-large);margin-top:var(--nu-space-milli);
    border-top:1px solid var(--nu-border-neutral);
    color:var(--nu-fg-secondary);
  }
  .foot-left{display:flex;align-items:baseline;gap:var(--nu-space-small);flex-wrap:wrap}
  .foot-mark{font:var(--nu-type-h3);color:var(--nu-fg-primary)}
  .foot-copy{color:var(--nu-fg-secondary)}
  .foot-links{display:flex;gap:var(--nu-space-medium)}
  .foot-link{font:inherit}

  /* ---------- design system docs ---------- */
  .ds{padding:0 var(--nu-space-small) var(--nu-space-max)}
  .ds-head{margin:var(--nu-space-micro) 0 var(--nu-space-medium)}
  .ds-layout{display:grid;grid-template-columns:210px 1fr;gap:var(--nu-gutter-column);align-items:start}
  .ds-nav{
    position:sticky;top:var(--nu-space-medium);display:flex;flex-direction:column;gap:var(--nu-space-nano);
    border-right:1px solid var(--nu-border-neutral);padding-right:var(--nu-space-medium);
  }
  .ds-nav-group{
    font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);text-transform:uppercase;
    color:var(--nu-fg-secondary);margin:var(--nu-space-medium) 0 var(--nu-space-micro);
  }
  .ds-nav-group:first-child{margin-top:0}
  .ds-nav-item{
    text-align:left;font:inherit;font-size:var(--nu-text-size-md);padding:var(--nu-space-nano) 0;
    color:var(--nu-fg-secondary);border-left:2px solid transparent;padding-left:var(--nu-space-milli);
    margin-left:calc(var(--nu-space-milli) * -1);
  }
  .ds-nav-item:hover{color:var(--nu-fg-primary)}
  .ds-nav-item.is-active{color:var(--nu-fg-primary);border-left-color:var(--nu-border-primary)}
  .ds-nav-item:focus-visible{outline:var(--nu-focus-ring) solid var(--nu-border-primary);outline-offset:var(--nu-focus-offset)}
  .ds-body{max-width:720px;min-width:0}
  @media (max-width:820px){
    .ds-layout{grid-template-columns:1fr;gap:var(--nu-space-medium)}
    .ds-nav{
      position:static;flex-direction:row;flex-wrap:wrap;gap:var(--nu-space-nano) var(--nu-space-small);
      border-right:0;border-bottom:1px solid var(--nu-border-neutral);
      padding:0 0 var(--nu-space-small);
    }
    .ds-nav-group{display:none}
    /* On a phone the fixed columns would leave the note a few characters wide.
       The bar is the one cell the number beside it already tells you, so it
       goes, and the rest is left to the browser to apportion. */
    .ds .space-table{table-layout:auto}
    .ds .space-table col.c-token,
    .ds .space-table col.c-scale{width:auto}
    .ds .space-table .sc .space-bar{display:none}
    .ds .sp-figs{grid-template-columns:1fr}
    .ds .ds-badges,
    .ds .ds-fields,
    .ds .ds-links,
    .ds .ds-buttons{grid-template-columns:repeat(2,1fr)}
    .ds-nav-item{margin-left:0;padding-left:0;border-left:0;border-bottom:2px solid transparent}
    .ds-nav-item.is-active{border-left-color:transparent;border-bottom-color:var(--nu-border-primary)}
  }
  .ds h1{font:var(--nu-type-h1);margin-bottom:var(--nu-space-milli)}
  /* The opening paragraph of every page runs a step above the body, so the eye
     lands on the summary before it reaches the tables. */
  .ds .ds-lede{
    font-size:var(--nu-text-size-lg);line-height:var(--nu-line-normal);
    color:var(--nu-fg-secondary);max-width:60ch;margin-bottom:var(--nu-space-medium);
  }
  /* The heading of a part gathering several sections under it. */
  .ds h2{font:var(--nu-type-h2);margin:var(--nu-space-huge) 0 var(--nu-space-milli)}
  .ds h3{font:var(--nu-type-h3);margin:var(--nu-space-medium) 0 var(--nu-space-milli)}
  /* Documentation prose only. A specimen embedded in these pages carries the
     shop's own class, and this rule outranks the component's – so a quote would
     render here with different spacing than it has in the shop, which is the one
     thing a worked example must never do. */
  .ds p:not([class]){max-width:62ch;margin-bottom:var(--nu-space-milli)}
  .ds table{width:100%;border-collapse:collapse;margin:var(--nu-space-milli) 0 var(--nu-space-medium);font-size:var(--nu-text-size-md)}
  .ds th,.ds td{
    /* Equal air above and below: the rule underneath a row reads as the end of
       that row, not as an underline stuck to its last line of text. */
    text-align:left;vertical-align:top;
    padding:var(--nu-space-micro) var(--nu-space-milli);
    line-height:var(--nu-line-normal);
    border-bottom:1px solid var(--nu-border-neutral);
  }
  /* A table nested inside a cell already ends where the parent row's rule falls,
     so its own last rule lands a hair above it and the two read as a mistake.
     Its bottom margin has to go for the same reason: inherited from .ds table it
     would push the parent row's rule further down than every other row's. */
  .ds td table{margin:var(--nu-space-micro) 0 0}
  .ds td table tr:last-child > td{border-bottom:0}
  .ds th{color:var(--nu-fg-secondary);font:var(--nu-type-body-s);
    letter-spacing:var(--nu-tracking-caps);text-transform:uppercase}
  /* typography scale table: style + token columns never wrap */
  .ds .type-table{table-layout:fixed}
  .ds .type-table col.c-style{width:15%}
  .ds .type-table col.c-token{width:29%}
  .ds .type-table col.c-family{width:11%}
  .ds .type-table td:nth-child(1),
  .ds .type-table td:nth-child(3){white-space:nowrap}
  /* the token cell may wrap between its parts, but never inside a token name */
  .ds .type-table td:nth-child(2){white-space:normal;overflow-wrap:anywhere}
  .ds .type-table td:nth-child(2) code{white-space:nowrap}
  /* semantic token tables: token + source columns never wrap */
  /* The ladder. A step is a row, and the only thing drawn differently is where
     the reader is in it: the one in progress carries the emphasis the whole
     table is read for, everything after it recedes. Closed steps stay at full
     strength, because what has been decided is the part that is worth reading. */
  .ds .rm-n{font-variant-numeric:tabular-nums;color:var(--nu-fg-secondary)}
  .ds .rm-state{color:var(--nu-fg-secondary)}
  .ds .rm-step.is-now{background:var(--nu-bg-secondary)}
  .ds .rm-step.is-now .rm-n,
  .ds .rm-step.is-now .rm-state{color:var(--nu-fg-primary)}
  .ds .rm-step.is-next td,
  .ds .rm-step.is-later td{color:var(--nu-fg-secondary)}
  .ds .rm-step.is-later{opacity:.72}

  /* The name of a document is a name, not a heading and not a link. It takes the
     weight of the text it labels and nothing else, so the column reads as a list
     of what exists rather than a column of things to click. */
  .ds .doc-table{table-layout:fixed}
  .ds .doc-table td.doc-name{
    white-space:normal;
    font-weight:var(--nu-weight-medium);color:var(--nu-fg-primary);
  }
  .ds .doc-table td code{font-size:0.885em;word-break:break-all}
  .ds .doc-table .rm-state{font-variant-numeric:tabular-nums;white-space:nowrap}

  .ds .tok-table{table-layout:fixed}
  .ds .tok-table col.c-token{width:32%}
  .ds .tok-table col.c-source{width:24%}
  .ds .tok-table td.spec code,
  .ds .tok-table td:nth-child(2) code{white-space:nowrap}
  /* Deliberately NOT a --nu- token: the monospace face is not part of the design
     system, it belongs to these documentation pages alone. Token names read
     badly in a proportional face – hyphens and underscores stop being legible –
     so both inline code and code blocks borrow one, declared here and nowhere
     else. The shop itself never loads or uses it. */
  .ds{
    --ds-font-mono:ui-monospace,SFMono-Regular,Menlo,monospace;
    /* Documentation chrome, not shop type: a code block is set in a face the shop
       never loads, and its line needs more air than any shop text does. Carries
       the --ds- prefix for the same reason the face does. */
    --ds-line-code:1.7;
  }
  .ds code{
    font-family:var(--ds-font-mono);font-size:0.885em;
    background:var(--nu-bg-secondary);padding:var(--nu-space-nano) var(--nu-space-nano);
  }
  .ds .ds-code{
    background:var(--nu-bg-secondary);
    padding:var(--nu-space-milli) var(--nu-space-small);
    margin:var(--nu-space-milli) 0 var(--nu-space-medium);
    font-family:var(--ds-font-mono);font-size:0.885em;line-height:var(--ds-line-code);
    white-space:pre;overflow-x:auto;
  }
  /* Live preview. The frame reuses .demo so the specimen stands on the same
     ground as every other one in these pages, and the two rows of options are
     chips - the shop's own component, not a second set of controls. */
  .ds .ds-play-row{
    display:flex;align-items:baseline;gap:var(--nu-space-small);
    margin-bottom:var(--nu-space-milli);
  }
  .ds .ds-play-lbl{
    flex:none;min-width:64px;
    font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
    text-transform:uppercase;color:var(--nu-fg-secondary);
  }
  .ds .ds-play-stage{margin-top:var(--nu-space-small);justify-content:center;min-height:88px}
  /* specimen rows sit centred in their frame, like the preview stage above */
  .ds .ds-chips,
  .ds .ds-links{justify-content:center}
  .ds .ds-code-head{
    display:flex;align-items:baseline;justify-content:space-between;gap:var(--nu-space-small);
    margin-top:var(--nu-space-medium);
  }
  .ds .ds-code-head .ds-play-lbl{min-width:0}
  .ds .ds-code-head + .ds-code{margin-top:var(--nu-space-micro)}
  .ds .swatch{
    width:18px;height:18px;border:1px solid var(--nu-border-neutral);
    display:inline-block;vertical-align:-4px;margin-right:var(--nu-space-micro);
  }
  .ds td.spec, .ds .spec{display:table-cell;white-space:nowrap}
  /* A section inside a category of the inventory: colour splits into the palette
     and the roles above it, typography into the scales and the styles.
     The weight comes after the shorthand, which resets it. */
  .ds h4{
    font:var(--nu-type-body-m);font-weight:var(--nu-weight-medium);
    margin:var(--nu-space-small) 0 var(--nu-space-micro);
  }
  .ds .demo{
    background:var(--nu-bg-secondary);padding:var(--nu-space-medium);margin:var(--nu-space-milli) 0 var(--nu-space-micro);
    display:flex;flex-wrap:wrap;gap:var(--nu-space-small);align-items:center;
  }
  .ds .demo.on-page{background:var(--nu-bg-primary);border:1px solid var(--nu-border-neutral)}
  /* The name pattern is the whole content of its box, so it sits in the middle
     of it rather than starting from the left edge like a row of specimens. */
  .ds .demo.ds-pattern{justify-content:center}
  /* A badge is absolutely positioned against a packshot, so a row of them laid
     out inline stacks in one corner. Each specimen gets its own tile, which is
     also where the reader will actually meet it. */
  /* Three across, so the five specimens fall into two rows. At five the tiles
     are narrower than the longest badge and its text breaks over two lines,
     which is not how the badge behaves anywhere in the shop. */
  /* Shared specimen grid: one figure per variant, each captioned, so the reader
     never has to match a row of examples against a table by position. */
  .ds .ds-specimens{
    display:grid;gap:var(--nu-space-small);
    margin:var(--nu-space-milli) 0 var(--nu-space-medium);
  }
  .ds .ds-specimens figure{margin:0}
  /* One sentence set at every step, so the steps are compared against each
     other rather than against different words. A step wider than the column is
     cut rather than wrapped: a second line would make it look like a different
     size. */
  .ds .ds-eg{display:block;max-width:340px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* One card per face: its name, what it is for, the letterforms at a size where
     the shapes can be judged, and the set they are drawn from - including the
     Polish diacritics, which is where a face either holds up or does not. Two
     cards side by side where there is room; auto-fit rather than a breakpoint,
     so the card decides from the space it has rather than from the viewport. */
  .ds .ds-faces{
    display:grid;gap:var(--nu-space-small);
    grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));
    margin:var(--nu-space-milli) 0 var(--nu-space-medium);
  }
  .ds .ds-face{
    background:var(--nu-bg-primary);border:1px solid var(--nu-border-neutral);
    padding:var(--nu-space-medium);overflow:hidden;
  }
  /* The card's own label stays in the documentation's voice: setting it in the
     face being shown would make the specimen judge itself. */
  .ds .ds-face-head{margin-bottom:var(--nu-space-medium)}
  .ds .ds-face-head .name{font-family:var(--ds-font-mono);font-size:0.885em}
  .ds .ds-face-aa{font-size:4.5rem;line-height:var(--nu-line-flat);margin-bottom:var(--nu-space-medium)}
  .ds .ds-face-set{font-size:var(--nu-text-size-lg);line-height:var(--nu-line-snug);overflow-wrap:anywhere}
  /* A style specimen carries its own name beside it, because a style is the one
     thing here that cannot be read off a value. */
  .ds .ds-style-row{display:flex;align-items:baseline;gap:var(--nu-space-small);margin-bottom:var(--nu-space-milli)}
  .ds .ds-style-row .lbl{
    flex:none;width:190px;
    font-family:var(--ds-font-mono);font-size:0.885em;color:var(--nu-fg-secondary);
  }
  .ds .ds-badges{grid-template-columns:repeat(3,1fr)}
  .ds .ds-fields{grid-template-columns:repeat(3,1fr)}
  .ds .ds-fields .demo{margin:0;display:block}
  .ds .ds-buttons,
  .ds .ds-links{grid-template-columns:repeat(2,1fr)}
  .ds .ds-buttons .demo,
  .ds .ds-links .demo{margin:0;justify-content:center;min-height:76px}
  /* A specimen only has to show the badge and its inset, so the tile drops the
     packshot's 4/5 ratio for a crop. Height, not spacing – hence a plain value.
     In the preview frame it also needs a width: the badge inside is positioned
     against the tile, so the tile has no content of its own to be sized by. */
  .ds .ds-specimens .tile{aspect-ratio:auto;height:88px}
  /* The badge preview wants a crop, the tile preview wants the real 4:5 - so the
     crop is asked for by name instead of being imposed on every tile in a stage. */
  .ds .ds-play-stage .tile{width:140px}
  .ds .ds-play-stage .tile.ds-crop{aspect-ratio:auto;width:240px;height:88px}
  .ds .ds-tiles,
  .ds .ds-qty{justify-content:center;align-items:flex-end}
  /* The author's mark carries the drawer's vertical margin, which in a row of
     specimens spreads the row instead of the column it was meant for. Asked for
     by name, as the sort panel is, rather than taken off every avatar. */
  .ds .ds-avatars{justify-content:center}
  .ds .ds-avatars .d-ava{margin:0}
  .ds .ds-select,
  .ds .ds-group{justify-content:center}
  /* The panel is positioned against the trigger and hidden until asked for. In a
     specimen it has to stand still and stay open, so the docs put it back in the
     flow by name rather than imposing it on every panel in a frame. */
  .ds .ds-sorts{grid-template-columns:repeat(2,1fr)}
  .ds .ds-sorts .demo{margin:0;display:flex;justify-content:center}
  .ds .ds-sort .sort{text-align:left}
  .ds .ds-sort .sort-menu{position:static;display:block;box-shadow:none}
  /* A curve drawn from the same numbers that drive the dot: the shape is the
     path, the movement is the timing function, and both read the token. Time
     runs left to right, distance covered runs bottom to top, and the dashed
     diagonal is linear for comparison. */
  .ds .ds-curve{display:block}
  .ds .ds-curve rect{fill:var(--nu-bg-primary);stroke:var(--nu-border-neutral);stroke-width:.5}
  .ds .ds-curve .ref{fill:none;stroke:var(--nu-border-neutral);stroke-width:.5;stroke-dasharray:2 2}
  .ds .ds-curve .crv{fill:none;stroke:var(--nu-fg-primary);stroke-width:1.25}
  .ds .ds-curve .dot{fill:var(--nu-fg-primary)}
  /* 2.4s is the graph's own pace, not a step of the motion scale: the dot has to
     complete its run often enough to be read while the table is on screen. */
  .ds .ds-curve .cx{animation:dsCurveX 2.4s linear infinite}
  .ds .ds-curve .cy{animation-name:dsCurveY;animation-duration:2.4s;animation-iteration-count:infinite}
  @keyframes dsCurveX{0%{transform:translateX(0)}60%{transform:translateX(64px)}100%{transform:translateX(64px)}}
  @keyframes dsCurveY{0%{transform:translateY(0)}60%{transform:translateY(-64px)}100%{transform:translateY(-64px)}}
  @media (prefers-reduced-motion:reduce){
    .ds .ds-curve .cx,
    .ds .ds-curve .cy{animation:none}
    .ds .ds-curve .dot{display:none}
  }
  .ds .ds-tiles .tile{width:140px}
  .ds .ds-specimens figcaption{
    font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
    text-transform:uppercase;color:var(--nu-fg-secondary);margin-top:var(--nu-space-micro);
  }
  .ds .note{color:var(--nu-fg-secondary);font-size:var(--nu-text-size-md);margin-bottom:var(--nu-space-medium)}
  /* same tint as the figures: a bar in this table is a measured distance too,
     and one reading for that across the whole tab */
  .ds .space-bar{background:var(--nu-bg-measure);height:12px;display:inline-block}
  /* Spacing scale table. Column widths, not spacing – so plain lengths, not
     scale steps: the name column has to hold the longest token on one line and
     the bar column the largest step on the scale. */
  .ds .space-table{table-layout:fixed}
  .ds .space-table col.c-token{width:172px}
  .ds .space-table col.c-scale{width:132px}
  .ds .space-table td:first-child code{white-space:nowrap}
  .ds .space-table .sc{display:flex;align-items:center;gap:var(--nu-space-micro)}
  .ds .space-table .sc b{font-weight:var(--nu-weight-normal);font-variant-numeric:tabular-nums;white-space:nowrap}
  .ds .space-table .none{color:var(--nu-fg-tertiary)}

  /* Spacing figures. One reading throughout: grey is the empty space being
     measured, an outlined block is a thing sitting next to it. Every distance
     is set from a live token, so a figure cannot disagree with the scale. */
  .ds .sp-figs{
    display:grid;grid-template-columns:1fr 1fr;
    gap:var(--nu-space-medium) var(--nu-gutter-column);
    margin:var(--nu-space-milli) 0 var(--nu-space-medium);
  }
  .ds .sp-cap{
    font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
    text-transform:uppercase;color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-micro);
  }
  .ds .sp-note{font-size:var(--nu-text-size-md);color:var(--nu-fg-secondary);margin-top:var(--nu-space-micro)}
  .ds .sp-flex{display:flex;align-items:stretch}
  .ds .sp-thing{flex:1;height:48px;border:1px solid var(--nu-border-primary);background:var(--nu-bg-primary)}
  .ds .sp-gap{background:var(--nu-bg-measure);flex:none}
  .ds .sp-pad{background:var(--nu-bg-measure)}
  /* the container carries the tint and the children cover it, so what shows
     through is exactly the gap – no second element pretending to be the space */
  .ds .sp-tiles{display:flex;background:var(--nu-bg-measure)}
  .ds .sp-tiles > div{flex:1;aspect-ratio:7/10;background:var(--nu-bg-secondary)}
  .ds .sp-groups{display:flex;flex-direction:column;background:var(--nu-bg-measure)}
  .ds .sp-groups > div{background:var(--nu-bg-primary)}
  .ds .sp-groups .lb{
    font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
    text-transform:uppercase;color:var(--nu-fg-tertiary);margin-bottom:var(--nu-space-nano);
  }
  .ds .sp-groups .ln{height:10px;background:var(--nu-bg-secondary)}
  /* Marks laid over the worked example. Their boxes come from measuring the
     rendered component, so they cannot show a distance the component does not
     have – and they sit behind the text, so nothing is obscured. */
  .ds .ds-eg{position:relative}
  .ds .ds-eg > *{position:relative;z-index:1}
  .ds .sp-mark{position:absolute;z-index:0;background:var(--nu-bg-measure);pointer-events:none}
  .ds .ico-cell{width:32px}

  /* ---------- responsive ---------- */
  @media (min-width:1280px){
    .shop{grid-template-columns:250px 1fr}
    .grid{grid-template-columns:repeat(4,1fr)}
  }
  @media (max-width:1100px){
    .grid{grid-template-columns:repeat(2,1fr)}
  }
  @media (max-width:820px){
    /* One value per breakpoint: the cover's relative width is a single decision,
       and the open transition zooms one and the same box, so grid and product
       page have to agree at every width. */
    :root{--nu-cover-width:72%}
    /* The block lets go here. A phone has too little height to keep three rows
       of it, and the controls over the grid have already moved to a bar pinned
       at the foot of the screen, which is where a thumb reaches. Letting go
       means no stacking context either, so that bar keeps answering to its own
       z-index rather than to this one's. */
    .masthead{position:static;z-index:auto}
    .masthead-veil{display:none}
    /* Nothing is holding the top of the window here, so there is nothing for the
       column to stop under, and it stands in the flow above the grid. */
    .filters{position:static}
    .grid{grid-template-columns:repeat(2,1fr);gap:var(--nu-space-nano)}
    .product{padding:0 var(--nu-space-milli) var(--nu-space-max)}
    .p-grid{grid-template-columns:1fr;gap:var(--nu-space-medium)}
    .p-tile{max-width:none}
    .p-info{max-width:none}
    .p-title{font-size:var(--nu-text-size-xl)}
    /* Full width here too, so it takes the taller of the two heights with it. */
    .p-cta{display:block;width:100%;text-align:center;padding:var(--nu-space-milli) 0}
    .co-grid{grid-template-columns:1fr;gap:var(--nu-space-large)}
    .co-side{position:static}
    .f-grid{grid-template-columns:1fr}
    /* Narrow enough that a row of fields would cost more than it explains, so
       the row unwinds: one field under another, subgrid off, ordinary gap on. */
    .f-addr{
      grid-template-columns:1fr;grid-template-rows:none;
      gap:var(--nu-space-milli) 0;
    }
    .f-addr .field{display:block}
    header{
      padding:var(--nu-space-small) var(--nu-space-milli) var(--nu-space-milli);
      flex-wrap:wrap;align-items:center;row-gap:var(--nu-space-milli);
    }
    .logo{order:1}
    .logo .mark{font-size:var(--nu-text-size-xl)}
    .logo .strap{display:none}
    .head-right{display:contents}
    .head-icons{order:2;gap:var(--nu-space-medium);margin-left:auto}
    /* The two blocks stay apart on their own row as well, one step narrower:
       at this width the header's own distance would push the scheme off the
       edge, and the row already has a rule above it doing part of the work. */
    .switchers{order:3;width:100%;justify-content:flex-start;gap:var(--nu-space-medium);
      border-top:1px solid var(--nu-border-neutral);padding-top:var(--nu-space-milli)}
    .sw-content{gap:var(--nu-space-small)}
    /* on mobile the filters collapse into a stacked panel above the grid */
    .shop{grid-template-columns:1fr;gap:0;padding:0 var(--nu-space-micro) var(--nu-space-huge)}
    .shop.filters-hidden{grid-template-columns:1fr}
    .filters{
      position:static;
      max-height:1000px;overflow:hidden;
      border-top:1px solid var(--nu-border-neutral);margin-bottom:var(--nu-space-small);padding-top:var(--nu-space-small);
      transition:max-height var(--nu-motion-slow) linear, opacity var(--nu-motion-slow) linear, margin var(--nu-motion-slow) linear, padding var(--nu-motion-slow) linear;
    }
    .shop.filters-hidden .filters{
      max-height:0;opacity:0;margin:0;padding:0;border-top-color:transparent;
    }
    /* Raising or dropping the sheet takes the panel in and out of the flow. That
       has to happen in one frame so the scroll can be corrected against it –
       an animated collapse would move the list for the whole transition afterwards. */
    .filters.no-anim{transition:none}

    /* ---------- mobile: sticky bottom bars ----------
       The shop bar leaves the top of the page and pins to the bottom, within
       thumb reach and clear of the first screen of covers. It is the SAME
       element as on desktop: the filter toggle, the sort button and the sort
       menu keep their existing handlers and state, so there is no second copy
       of these controls to keep in sync. */
    .shopbar{
      /* Back to a row here: the field takes a line of its own, because three
         controls across this width would leave it too narrow to read what is in
         it. measureBars re-reads the height, so the clearance under the grid
         follows on its own. */
      display:flex;justify-content:space-between;
      flex-wrap:wrap;row-gap:var(--nu-mobar-padding);
      /* above the sheet backdrop (70): the bar holds the controls that opened
         the sheet, so dimming them would be dimming the way out. Still below
         the drawers (90/100), which are meant to cover it. */
      position:fixed;left:0;right:0;bottom:0;z-index:85;
      /* No padding at the top: the field's own is already there, and two
         spacings meaning the same thing add up to one that reads as a mistake.
         The desktop bar has none either, so the two layouts agree. */
      padding:0 var(--nu-space-small) calc(var(--nu-mobar-padding) + env(safe-area-inset-bottom));
      gap:var(--nu-space-small);
      background:var(--nu-bg-primary);
      border-top:1px solid var(--nu-border-neutral);
      box-shadow:0 -6px 18px color-mix(in srgb, var(--nu-bg-shadow) 5%, transparent);
    }
    /* clearance for whichever bar this view actually shows, so the footer and
       the last form field are never trapped underneath it */
    body.has-mobar{padding-bottom:var(--nu-mobar-height)}
    body.has-cobar{padding-bottom:var(--nu-cobar-height)}
    /* The menu has nowhere to go but up now. It is anchored to the sort button,
       so clear the bar's own padding as well to land on the same gap above the
       bar as the filter sheet. */
    .sort-menu{top:auto;bottom:calc(100% + var(--nu-mobar-padding) + var(--nu-mobar-gap))}
    /* a long sort label must not push the bar into two lines */
    .sort-btn{max-width:62vw}
    /* On its own row the field takes the whole width, so its two desktop widths
       and the change between them do not apply here. */
    .search-wrap{order:-1;flex:1 0 100%}
    .search-wrap .input.in-bar,
    .search-wrap .input.in-bar:focus{width:100%}
    .sort-btn #sortLbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

    /* ---- filters raised as a bottom sheet ----
       Still the inline panel, only positioned differently: position:fixed takes
       it out of the flow, so the list closes up behind it and a chip tapped in
       the sheet is already the selected chip in the inline panel. */
    body.fsheet .shop .filters{
      /* top:auto is load-bearing: the panel carries top:var(--nu-space-medium) for its sticky
         desktop column, and leaving it set here would over-constrain the box
         and stretch the sheet from the top of the screen instead. */
      /* Przylega do belki: obie mają się czytać jako jeden blok kontrolek.
         Dlatego wysokość niezaokrąglona - ta z siatki bywa do czterech pikseli
         wyższa od prawdziwej belki i zostawia pod arkuszem pasek tła strony. */
      position:fixed;left:0;right:0;top:auto;z-index:80;
      bottom:var(--nu-mobar-height-exact);
      max-height:min(62vh,460px);overflow-y:auto;
      /* filters-hidden is set while the sheet is up (it collapses the inline
         copy), and that rule also parks pointer-events – hand them back, or the
         chips inside the sheet are unclickable. */
      margin:0;padding:var(--nu-space-medium) var(--nu-space-small);opacity:1;pointer-events:auto;
      /* the inline panel animates its own max-height to collapse; here the
         slide does the work, so that transition must not run as well */
      transition:none;
      background:var(--nu-bg-primary);
      border-top:1px solid var(--nu-border-neutral);
      box-shadow:0 -14px 40px color-mix(in srgb, var(--nu-bg-shadow) 10%, transparent);
      animation:sheetUp var(--nu-motion-slow) var(--nu-ease-slide);
    }
    /* closing: drop it past the bar before it goes back into the flow */
    body.fsheet.fsheet-out .shop .filters{
      animation:none;
      transform:translateY(calc(100% + var(--nu-mobar-height)));
      transition:transform var(--nu-motion-base) ease;
    }
    body.fsheet .fsheet-head{
      display:flex;align-items:center;justify-content:space-between;
      font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
      text-transform:uppercase;color:var(--nu-fg-secondary);
    }
    /* colour, hover and focus come from .btn-tertiary, as on both drawers */
    .fsheet-head button{padding:0 var(--nu-space-nano)}

    /* ---- checkout: amount due and the one committing action ---- */
    .co-bar{
      display:flex;align-items:center;justify-content:space-between;gap:var(--nu-space-small);
      position:fixed;left:0;right:0;bottom:0;z-index:60;
      padding:var(--nu-space-milli) var(--nu-space-small) calc(var(--nu-space-milli) + env(safe-area-inset-bottom));
      background:var(--nu-bg-primary);
      border-top:1px solid var(--nu-border-neutral);
      box-shadow:0 -6px 18px color-mix(in srgb, var(--nu-bg-shadow) 5%, transparent);
    }
    .co-bar .cb-lbl{
      display:block;font:var(--nu-type-body-s);letter-spacing:var(--nu-tracking-caps);
      text-transform:uppercase;color:var(--nu-fg-secondary);margin-bottom:var(--nu-space-nano);
    }
    .co-bar .cb-val{font-size:var(--nu-text-size-lg);font-variant-numeric:tabular-nums} /* size only: see .p-price */
    /* Back to its own width in the bar, so it is an inline button again and
       takes the shorter of the two heights. */
    .co-bar .order-btn{width:auto;margin-top:0;padding:var(--nu-space-micro) var(--nu-space-medium);flex-shrink:0}
    /* Tylko w kasie: tam przycisk paska wysyła formularz, więc ten w
       podsumowaniu byłby powtórzonym submitem. W koszyku żadnego formularza
       nie ma, a paska na dole też nie - reguła bez tego zawężenia zostawiała
       telefon bez jakiejkolwiek drogi dalej. */
    #checkout .co-side .order-btn{display:none}
  }
  @keyframes sheetUp{from{transform:translateY(100%)}to{transform:none}}
  @media (prefers-reduced-motion:reduce){
    .filters{transition:none}
    body.fsheet .shop .filters{animation:none}
    body.fsheet.fsheet-out .shop .filters{transition:none}
    .sheet-backdrop{transition:none}
  }
  @media (max-width:380px){
    :root{--nu-cover-width:58%}
    .grid{grid-template-columns:1fr}

  }
