@layer components {
  /* Properties down the side, nights across the top.
   *
   * It wears .screen as well, which is where the flex column, the floor, the
   * canvas, the toolbar across the top and the scroller under it live — this
   * screen, the inbox and the properties list all had their own copy of those
   * before there were three of them. What is here is only the grid.
   *
   * The grid is still the only thing that scrolls, in either axis: the date
   * bands stick against .screen__scroll, and a page scrolling as well would
   * carry them out of the window on the way down. It was a card with a bounded
   * viewport inside a page with its own scrollbar, which read as a calendar
   * somebody had put on a screen instead of as the screen — two scrollbars to
   * work out, and an edge on all four sides of the one thing the screen is for.
   *
   * Every band — the months, the dates, a row's nights, a row's bars — is its
   * own grid of fixed-width implicit columns. Nothing declares how many nights
   * there are, so a night can be appended to the end of a band and the band
   * simply gets wider. That is what lets the window grow on scroll without
   * JavaScript writing a style.
   *
   * It also keeps the two layers of a row in agreement without them agreeing
   * on a count: every column is --calendar-day-width, so column 40 starts at
   * the same offset in a layer that has 40 of them and a layer that has 90.
   */
  .calendar {
    --calendar-day-width: 5.25rem;
    --calendar-label-width: 15rem;
    /* The column with its names given up: a thumbnail and the padding around
       it, derived rather than measured, so changing the thumbnail moves it. */
    --calendar-collapsed-label-width: calc(var(--calendar-thumbnail-size) + var(--calendar-cell-padding-inline) * 2);
    /* .visually-hidden's, because a class cannot be conditional on a modifier —
       see the collapsed rules below. */
    --calendar-hidden-size: 1px;
    --calendar-row-height: 3.5rem;
    --calendar-month-height: 1.75rem;
    --calendar-cell-padding-block: 0.6rem;
    --calendar-cell-padding-inline: 0.9rem;
    --calendar-thumbnail-size: 2.5rem;
    --calendar-bar-height: 1.9rem;
    --calendar-bar-gap: 0.2rem;
    --calendar-bar-slant: 0.7rem;
    /* Columns per night in the bars layer. Two, and the model agrees — see
       Calendar::Row::HALVES_PER_NIGHT. */
    --calendar-bar-halves: 2;
    --calendar-bar-padding-inline: 0.6rem;
    --calendar-marker-size: 0.375rem;
    --calendar-placeholder-line: 0.9rem;
    --calendar-placeholder-name-width: 8rem;
  }

  /* The two shapes of line: a label pinned to the start edge, and a track of
     nights beside it. */
  .calendar__head,
  .calendar__row {
    display: grid;
    grid-template-columns: var(--calendar-label-width) 1fr;
    min-inline-size: max-content;
  }

  .calendar__row:not(:last-child) {
    border-block-end: var(--border);
  }

  /* A page of properties, and a <turbo-frame>, which is an unknown element and
     inline until told otherwise. It has to keep a box: `display: contents`
     would give Turbo's IntersectionObserver nothing to watch and the lazy page
     below would never load. max-content so a frame is as wide as the rows in
     it rather than as wide as the viewport. */
  .calendar__page {
    display: block;
    min-inline-size: max-content;
  }

  /* A row that hasn't arrived. Its height is a real row's, which is the whole
     job: the scroll position stays where the operator left it when the response
     lands. */
  .calendar__row--placeholder {
    align-items: center;
    min-block-size: var(--calendar-row-height);
  }

  .calendar__placeholder-thumbnail {
    inline-size: var(--calendar-thumbnail-size);
    block-size: var(--calendar-thumbnail-size);
    flex-shrink: 0;
  }

  .calendar__placeholder-name {
    inline-size: var(--calendar-placeholder-name-width);
    block-size: var(--calendar-placeholder-line);
  }

  .calendar__placeholder-nights {
    /* Night count comes from the view as data; the layout rule lives here. */
    inline-size: calc(var(--calendar-day-width) * var(--calendar-placeholder-nights));
    block-size: var(--calendar-placeholder-line);
    margin-inline: var(--calendar-cell-padding-inline);
  }

  .calendar__head {
    /* A third column the rows have no use for: the trigger that fetches the
       next stretch of nights sits past the end of the track. Without it that
       trigger wraps to a second row of its own and stops being the right edge
       of anything. */
    grid-template-columns: var(--calendar-label-width) 1fr auto;
    position: sticky;
    inset-block-start: 0;
    z-index: var(--z-sticky-header);
    background: var(--color-canvas);
    border-block-end: var(--border);
  }

  /* One area. The head stacks its two bands as rows; a property row overlays
     its two layers in the same area, so the explicitly placed bars cannot
     displace the auto-placed nights. */
  .calendar__track {
    display: grid;
  }

  .calendar__months,
  .calendar__days,
  .calendar__nights {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: var(--calendar-day-width);
  }

  /* The same grid at half the pitch: two columns to a night, so a stay can start
     in the middle of the day it arrives and stop in the middle of the day it
     leaves. Every column is still a fixed width, so a bar placed at column 57
     lands at the same offset in a layer holding sixty of them and one holding
     two hundred — which is what lets the window grow without anything being
     redrawn. Calendar::Row::HALVES_PER_NIGHT is the other half of this. */
  .calendar__bars {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: calc(var(--calendar-day-width) / var(--calendar-bar-halves));
  }

  .calendar__nights,
  .calendar__bars {
    grid-area: 1 / 1;
  }

  /* One grid row per lane, so overlapping bars stack instead of hiding each
     other. `align-content: center` keeps the stack in the middle of the row, so
     a row with one bar looks exactly as it did before lanes existed. */
  .calendar__bars {
    align-items: center;
    align-content: center;
    grid-auto-rows: var(--calendar-bar-height);
    row-gap: var(--calendar-bar-gap);
    pointer-events: none;
  }

  .calendar__corner,
  .calendar__label {
    position: sticky;
    inset-inline-start: 0;
    z-index: var(--z-sticky-cell);
    background: var(--color-canvas);
    border-inline-end: var(--border);
    padding: var(--calendar-cell-padding-block) var(--calendar-cell-padding-inline);
  }

  /* The one control in the header that is about the column rather than a night.
     Pinned to the start of the corner so it stays where it is when the column
     narrows underneath it. */
  .calendar__labels-toggle {
    margin-inline-start: calc(var(--calendar-cell-padding-inline) * -0.5);
  }

  /* Months */
  .calendar__month {
    /* Span comes from the view as data; the layout rule lives here. */
    grid-column: span var(--calendar-month-span);
    display: grid;
    align-content: center;
    block-size: var(--calendar-month-height);
    border-inline-start: var(--border);
    /* Deliberately not `overflow: hidden`. Clipping here makes this cell the
       scrollport its own sticky name measures against, and a scrollport that
       never scrolls holds nothing in place — the name below would scroll away
       with the month's first night, which is the one thing it must not do. */
  }

  /* A month wider than the window would otherwise lose its name off the start
     edge, leaving the band unlabelled for as long as that month lasts. */
  .calendar__month-name {
    position: sticky;
    inset-inline-start: var(--calendar-label-width);
    justify-self: start;
    /* Inset from the start edge only. There is nothing on the label's other
       side to clear, and the padding it does not have is what lets it fit a
       month contributing one night. */
    padding-inline-start: var(--calendar-cell-padding-inline);
    font-size: var(--text-xx-small);
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-x-wide);
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--color-ink-soft);
  }

  /* Dates */
  .calendar__day {
    display: grid;
    justify-items: center;
    align-content: center;
    gap: var(--space-eighth);
    padding-block: var(--space-half);
    border-inline-start: var(--border);
  }

  .calendar__weekday {
    font-size: var(--text-xx-small);
    text-transform: uppercase;
    letter-spacing: var(--tracking-x-wide);
    color: var(--color-ink-soft);
  }

  .calendar__date {
    font-size: var(--text-body);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
  }

  .calendar__day--weekend {
    background: color-mix(in oklch, var(--color-highlight) 55%, transparent);
  }

  .calendar__day--today {
    background: var(--color-highlight);

    .calendar__date,
    .calendar__weekday {
      color: var(--color-accent);
    }
  }

  /* The label column */
  .calendar__label {
    display: flex;
    align-items: center;
  }

  /* The whole label is the link, mark included, so the column still opens a
     property when its names are gone. */
  .calendar__identity {
    display: flex;
    align-items: center;
    gap: var(--space-half);
    min-inline-size: 0;
    color: inherit;
    text-decoration: none;
  }

  @media (any-hover: hover) {
    .calendar__identity:hover .calendar__property {
      text-decoration: underline;
    }
  }

  /* Thumbnails alone. One property decides the width and the markup is the same
     either way, which is the sidebar rail's arrangement and for the same reason:
     nothing that renders a row knows the state it is in.
     
     The names go out of view rather than out of the page. A row's only link
     would otherwise be a picture with no alt text, which is a link called
     nothing — so this is .visually-hidden's property set, inline because a class
     cannot be conditional on a modifier. */
  .calendar--collapsed-labels {
    --calendar-label-width: var(--calendar-collapsed-label-width);
  }

  .calendar--collapsed-labels .calendar__detail {
    position: absolute;
    inline-size: var(--calendar-hidden-size);
    block-size: var(--calendar-hidden-size);
    margin: calc(var(--calendar-hidden-size) * -1);
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* A row that hasn't arrived has no name to hide, only the box where one will
     be, and a box nobody can see is worth nothing to anybody. */
  .calendar--collapsed-labels .calendar__placeholder-name {
    display: none;
  }

  .calendar__thumbnail {
    inline-size: var(--calendar-thumbnail-size);
    block-size: var(--calendar-thumbnail-size);
    border-radius: var(--radius-small);
    object-fit: cover;
    flex-shrink: 0;
  }

  .calendar__thumbnail--empty {
    background: var(--color-highlight);
  }

  .calendar__detail {
    display: grid;
    min-inline-size: 0;
  }

  .calendar__property {
    font-weight: var(--weight-semibold);
    font-size: var(--text-normal);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .calendar__location {
    font-size: var(--text-x-small);
    color: var(--color-ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Nights.
   *
   * What a cell holds sits at its top and against its end edge: a price is a
   * figure in a column of figures, and figures line up on the right — the same
   * reason .table__number ends its column. Centred, every price sat on a
   * different x depending on how many digits it had, and the minimum stay under
   * it moved with it. */
  .calendar__cell {
    display: grid;
    justify-items: end;
    align-content: start;
    gap: var(--space-eighth);
    padding: var(--space-quarter) var(--space-half);
    min-block-size: var(--calendar-row-height);
    border-inline-start: var(--border);
    font-variant-numeric: tabular-nums;
  }

  .calendar__cell--weekend {
    background: color-mix(in oklch, var(--color-highlight) 35%, transparent);
  }

  .calendar__cell--today {
    background: color-mix(in oklch, var(--color-highlight) 75%, transparent);
  }

  /* A step down from a row's name and a step up in weight, because it is read as
     a column of figures rather than as a word: smaller keeps four columns of
     them on screen, and the weight is what stops the number receding with the
     size. Money#to_short_s drops the empty cents that were most of its width. */
  .calendar__price {
    display: flex;
    align-items: center;
    gap: var(--space-eighth);
    font-size: var(--text-small);
    font-weight: var(--weight-medium);
    color: var(--color-ink);
  }

  /* How many nights a guest has to take. Under the price and quieter than it —
     the price is what the operator scans for — and a moon rather than the word
     "min", which was the same three characters in every cell that had one. The
     number carries the name; the icon is decoration. */
  .calendar__stay {
    display: flex;
    align-items: center;
    gap: var(--space-eighth);
    font-size: var(--text-xx-small);
    color: var(--color-ink-soft);
  }

  /* The same mark standing in a sentence, where the legend says what it means.
     Inline-flex has no baseline worth using, so it is aligned rather than
     flowed. */
  .calendar__moon {
    display: inline-block;
    vertical-align: middle;
    color: var(--color-ink-soft);
  }

  /* This night is priced at more than one occupancy and the cell can only
     show one of them. What the dot means is spelled out under the grid, not
     in a hover: a touch screen has none, and this is information that exists
     nowhere else on the screen. */
  .calendar__shared {
    inline-size: var(--calendar-marker-size);
    block-size: var(--calendar-marker-size);
    border-radius: var(--radius-circle);
    background: var(--color-accent);
    flex-shrink: 0;
  }

  /* Being dragged over. An inset ring rather than a border: a border would add
     to the cell's box and shove every night after it sideways by a pixel as the
     drag passed, which reads as the grid shivering. */
  .calendar__cell[data-selected] {
    background: color-mix(in oklch, var(--color-accent) 12%, transparent);
    box-shadow: inset 0 0 0 var(--border-width-thick) var(--color-accent);
  }

  /* What the drag adds up to, beside the cell the pointer is over. Fixed, so it
     is measured against the window and not against a grid that is scrolled;
     pointer-events none, so it cannot become the thing the pointer is over and
     stop the drag reaching the cell underneath. */
  .calendar__chip {
    --calendar-chip-inset-inline: 0;
    --calendar-chip-inset-block: 0;
    --calendar-chip-lift: 2.25rem;
    /* Room for the longest thing it can say — "365 nights · $999,999.00" — so a
       drag that ends against the right edge of the window is held inside it
       rather than cut off. Cheaper than measuring: the text is bounded, so the
       reservation can be too. */
    --calendar-chip-reserve: 14rem;

    position: fixed;
    inset-inline-start: min(var(--calendar-chip-inset-inline), 100dvw - var(--calendar-chip-reserve));
    inset-block-start: calc(var(--calendar-chip-inset-block) - var(--calendar-chip-lift));
    z-index: var(--z-popup);
    margin: 0;
    padding: var(--space-quarter) var(--space-half);
    border-radius: var(--radius-small);
    background: var(--color-ink);
    color: var(--color-canvas);
    font-size: var(--text-small);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
  }

  /* The way into a stay, and the reason it is here rather than on the bar: the
     bars layer is `pointer-events: none` and has to stay that way, so the cell is
     the only part of a booked night that can be pressed. See the note on
     .calendar__bar.
   *
     It fills the cell and draws nothing — the bar above it is already the label,
     and a second mark under one would read as something else on the night. What
     it does draw is the affordance the bar cannot: a cursor, and a wash on hover
     saying which night the pointer is on. The wash is the same one the sidebar
     and an interactive table row use, so pointing at a night means one thing
     everywhere. */
  /* A booked cell gives up its inset and lets its one child have the whole box.
     There is nothing in it to inset — the bar in the layer above is the label —
     and a wash that stopped short of the cell's edges would read as a mark on the
     night rather than as the night being pointed at. */
  .calendar__cell:has(.calendar__booked) {
    align-content: stretch;
    padding: 0;
  }

  .calendar__booked {
    /* Filling the cell as a grid item rather than as an absolutely positioned
       overlay, and that is not a preference: making the cell a containing block
       makes it a positioned element, and positioned elements paint after every
       non-positioned one in the row — so the tint on a weekend or on today would
       come out on top of the bars layer and hide the very stay this is the way
       into. */
    justify-self: stretch;
    /* The cursor is the whole of the affordance, and washing the cell on hover
       was worse than nothing. Every other night on this grid answers a hover with
       no tint at all, and the one thing that *does* tint a cell is being dragged
       over — so a stay lighting up under the pointer said "you are selecting
       this" about the one night a drag must not reach. A pointer over a bar is
       already the web saying "this opens something". */
    cursor: pointer;

    /* The grid draws no outline of its own anywhere, so a focus ring has to be
       inset rather than around: an outline on a cell hard against its neighbours
       is clipped on two sides by them. */
    &:focus-visible {
      outline: none;
      box-shadow: inset 0 0 0 var(--border-width-thick) var(--color-accent);
    }
  }

  /* Bars share the bars layer's half-day tracks, so a five-night block is ten
     columns wide and a stay that leaves on the Sunday morning stops halfway
     through it.
   *
   * Every bar leans, and the lean is what a changeover day looks like: the guest
   * leaving and the guest arriving share that date, one taking its morning and
   * the other its afternoon, so the two diagonals meet along the middle of the
   * day and interlock across it. Two pills there would say the day belongs to
   * neither of them.
   *
   * Each end of the slant is its own property, so an end that runs past the
   * window can give it up — a diagonal there would read as a stay finishing at
   * the edge of the screen rather than carrying on past it. The slant is taken
   * out of the box rather than off it, so the label is inset by it and never
   * sits under the diagonal. */
  .calendar__bar {
    --calendar-bar-slant-start: var(--calendar-bar-slant);
    --calendar-bar-slant-end: var(--calendar-bar-slant);

    /* Column and span come from the view as data; the layout rule lives here. */
    grid-column: var(--calendar-bar-column) / span var(--calendar-bar-span);
    /* The lane, from the view. Every bar used to be `grid-row: 1`, so an
       overlapping stay was drawn on top of another one and hid it — and a night
       with a hidden confirmed booking under a visible enquiry looks free. */
    grid-row: var(--calendar-bar-lane, 1);
    align-self: center;
    /* A bar inherits `pointer-events: none` from .calendar__bars and keeps it.
       It is a label, not a control — and it sits centred over the middle half
       of every night it covers, in a layer that is not inside a cell. Taking
       the pointer meant a drag released over a blocked night stopped short of
       where it was released and silently opened the editor on a shorter range,
       while a tap on one did nothing at all — so "open for booking" could not
       reach the blocks it exists to cut. Anything here that needs a pointer
       has to become a real control in the nights layer. */
    margin-inline: var(--calendar-bar-gap);
    padding-inline: calc(var(--calendar-bar-padding-inline) + var(--calendar-bar-slant));
    block-size: var(--calendar-bar-height);
    display: flex;
    align-items: center;
    clip-path: polygon(
      var(--calendar-bar-slant-start) 0,
      100% 0,
      calc(100% - var(--calendar-bar-slant-end)) 100%,
      0 100%
    );
    font-size: var(--text-small);
    font-weight: var(--weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .calendar__bar--open-start {
    --calendar-bar-slant-start: 0rem;
  }

  .calendar__bar--open-end {
    --calendar-bar-slant-end: 0rem;
  }

  .calendar__bar--blocked {
    background: var(--surface-sunk);
    color: var(--color-ink);
  }

  /* A stay, toned by what state it is in: a confirmed booking and an enquiry
     hold the same nights today and mean very different things about tomorrow.
     The tone comes from Booking::State, so no screen can disagree with another
     about whether "Pending" is good news — and the surface/ink pairs are the
     badge's, so a stay on the calendar and a stay in a list read the same. */
  .calendar__bar--booking {
    background: var(--surface-sunk);
    color: var(--color-ink);
  }

  .calendar__bar--booking--positive {
    background: var(--surface-positive);
    color: var(--color-positive-ink);
  }

  .calendar__bar--booking--warning {
    background: var(--surface-warning-strong);
    color: var(--color-warning-ink);
  }

  .calendar__bar--booking--negative {
    background: var(--surface-negative);
    color: var(--color-negative);
  }

  /* A bar running past the edge of the window also gives up the gap it would
     have left there, so it reaches the edge rather than stopping short of it. */
  .calendar__bar--open-start {
    margin-inline-start: 0;
  }

  .calendar__bar--open-end {
    margin-inline-end: 0;
  }

  /* The right edge of the grid, and nothing else: no appearance and no
     content, just something that has to come into view before the next stretch
     of nights is worth fetching. It sits in the header band, which is sticky,
     so it is reachable at any vertical scroll position.
     Narrow enough to add no column anybody notices, wide enough to have a box
     an IntersectionObserver can measure. */
  .calendar__more {
    inline-size: var(--space-eighth);
  }

}
