@layer reset, base, components, utilities;

/* The two faces, served by us.
 *
 * Figtree for text, Roboto Mono for figures — money, dates, and the codes an
 * operator reads off a screen and types into a portal. A monospaced
 * face is not decoration on those: it is the only way a column of rates lines
 * up digit under digit and a property code can be checked character by
 * character without counting.
 *
 * Self-hosted rather than linked from Google, so a screen does not wait on a
 * third party to render its own text and no operator's IP address is handed to
 * one to load a font. Two subsets each — latin and latin-ext — which is Google's
 * own split and covers every market we sell into; a font-display of swap
 * means the system face draws first and the page never blocks on either file.
 *
 * Figtree is one variable file per subset carrying the whole 400–800 range,
 * which is why there is one @font-face per subset rather than one per weight.
 *
 * The src is a bare filename with no directory in it, and that is propshaft
 * rather than a mistake: every load path is one flat namespace, so a font in
 * app/assets/fonts is addressed by its name alone. A "../fonts/" that reads
 * correctly to a human resolves to nothing, is left in the output untouched,
 * and 404s at request time — which is exactly what happened first.
 */
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/assets/figtree-latin-f7c28c58.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/assets/figtree-latin-ext-ce5692a4.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Roboto Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/assets/roboto-mono-latin-8e08a3bc.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Roboto Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("/assets/roboto-mono-latin-ext-eeb4582c.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Design tokens.
 *
 * Every value a component uses comes from here. A raw px/rem literal in a
 * component stylesheet is a defect, not a shortcut: it cannot be themed, it
 * cannot be varied, and the next component copies it. Our previous PMS shipped
 * a theme config that was never populated and accumulated 1,536 arbitrary
 * values and 1,025 raw hex colors around it. The tokens below exist so that
 * reaching for the right value is cheaper than typing a literal.
 *
 * When a component needs a dimension of its own — a calendar day column, a
 * stepper marker, a thumbnail — it declares a component-scoped custom property
 * at the top of its own block rather than adding a global token:
 *
 *   .tabs          { --tabs-text: var(--text-body); }
 *   .tabs--compact { --tabs-text: var(--text-small); }
 *
 * That is how a component gets variants without its partial growing
 * conditionals: the modifier overrides the property, the markup never changes.
 * See VIEWS.md.
 */

/* How far orientation_helpers has slid an anchored surface back inside the
 * window. It is written on the element by JavaScript and read by popover.css
 * and tooltip.css, so unlike everything below it is not a value anyone chooses.
 *
 * Declared rather than defaulted at each use because `var(--x, 0px)` is a raw
 * literal wherever it appears, and it appears in every component that anchors
 * anything. One initial-value here is one place for it to be zero, and typing
 * it as a <length> means a bad write is ignored rather than inherited as a
 * string that breaks a translate.
 */
@property --orient-shift {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

:root {
  --font-sans: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* Figures, not code. A rate, a date, a listing id — anything read
     digit by digit or checked against another line. The system stack behind it
     is what draws while the woff2 is in flight. */
  --font-mono: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Type scale, on the comp's ladder.
     Its four small steps each sat about half a pixel above ours — invisible on
     any one label, and the difference between a dense list that reads and one
     that is squinted at. The comment on each step is what the comp uses it for,
     because "x-small" says how big it is and not what it is for. */
  --text-xx-small: 0.72rem;   /* 11.5 — column heads, section headings, status */
  --text-x-small: 0.78rem;    /* 12.5 — a row's meta line, a timestamp */
  --text-small: 0.84rem;      /* 13.5 — buttons, pills, a subline */
  --text-normal: 0.9rem;      /* 14.5 — nav items, fields, a row's name */
  --text-body: 1rem;
  --text-medium: 1.125rem;
  --text-large: 1.5rem;
  --text-x-large: 2rem;       /* 32 — a page title */

  /* Four weights the comp actually uses, and one in between that we do.
     --bold was 650, which is where this app's headings were quieter than the
     comp's at every size: 700 is its emphasis weight and 800 is what a page
     title is set in. Both are real instances rather than synthesised, because
     Figtree is one variable file carrying the whole range. */
  --weight-normal: 400;
  --weight-medium: 550;
  --weight-semibold: 600;
  --weight-bold: 700;
  --weight-heavy: 800;

  /* --tighter is for display sizes only. Tracking that reads as confident at
     32px reads as cramped at 16, which is why there are two of them. */
  --tracking-tighter: -0.03em;
  --tracking-tight: -0.015em;
  --tracking-normal: 0.02em;
  --tracking-wide: 0.04em;
  --tracking-x-wide: 0.06em;

  --leading-none: 1;
  --leading-normal: 1.5;
  --leading-loose: 1.6;

  /* Spacing — every step is a multiple of --space, so the gutters of the whole
     app move together when --space moves. Don't hardcode a step; derive it. */
  --space: 1rem;
  --space-eighth: calc(var(--space) / 8);
  --space-quarter: calc(var(--space) / 4);
  --space-half: calc(var(--space) / 2);
  --space-three-quarters: calc(var(--space) * 0.75);
  --space-and-quarter: calc(var(--space) * 1.25);
  --space-and-half: calc(var(--space) * 1.5);
  --space-double: calc(var(--space) * 2);
  --space-two-and-half: calc(var(--space) * 2.5);
  --space-triple: calc(var(--space) * 3);

  /* Cohosting palette: a white field with near-black ink and a deep green for
     anything that commits the operator to something.
     Every value below is measured off the Property Details v2 comp rather than
     chosen, so a screen can be compared against it rather than argued about.

     The field is the one value that is not the comp's. It was a soft sage —
     oklch(94% 0.012 138) — and at the size of a page that is not a neutral, it
     is a colour: four screens of white cards standing on it read as green
     screens. It is white now, the same white as the canvas.

     Which moves the whole job of separating a card from its page onto
     --shadow-card, and that is why the shadow below is heavier than the comp's.
     A field a step under the canvas does that work for free; a field the same
     white does none of it, and the comp's 5%-and-3% alpha is invisible against
     white. The two values are one decision and have to move together.

     Still one token, and every theme in _themes.css sets its own — which is what
     makes this a value somebody can change rather than a decision baked into
     four stylesheets. The palette in the corner of every development screen is
     how to look at it. */
  --color-bg: oklch(100% 0 0);
  --color-canvas: oklch(100% 0 0);
  --color-canvas-translucent: oklch(100% 0 0 / 90%);
  /* Near-black, and barely cool. This carried 0.02 chroma at hue 160 before,
     which is enough green to see: a heading read as very dark sage rather than
     as ink, and every neutral derived from it came out tinted. */
  --color-ink: oklch(21% 0.008 250);
  /* Secondary text — a row's meta line, a field's hint, a timestamp.
     The comp puts this at 66% lightness, and we deliberately don't: that is
     3.9:1 on white, under AA for body text, and this is the tone every list in
     the app explains itself in. 56% is 4.9:1 and still four steps softer than
     the 52% it was. */
  --color-ink-soft: oklch(56% 0.016 145);
  /* Two, because the comp has two and using one for both is most of why this
     app reads hard. An edge between a surface and the page is not the same
     weight as a rule between two rows inside one surface — a table drawn with
     the former looks like a spreadsheet grid. */
  --color-border: oklch(93% 0.008 140);
  --color-border-faint: oklch(96% 0.005 140);
  --color-accent: oklch(34.5% 0.068 157);
  --color-accent-ink: oklch(99% 0 0);
  --color-negative: oklch(52% 0.19 25);
  /* Amber, not yellow. At hue 80 the warning ink had to go dark to stay legible
     and read as olive on its own tint; 61 is the comp's amber, which holds its
     identity at both ends. */
  --color-warning: oklch(64% 0.14 61);
  --color-warning-ink: oklch(52% 0.11 61);
  /* One green, in two weights. The comp has no second green: a live listing
     wears the same deep green the primary button does, and the brighter mint we
     had was a colour that appeared nowhere else in the product. --positive is
     the solid — a dot, a border; --positive-ink is the accent, because ink on a
     tint is exactly where the comp puts the accent. */
  --color-positive: oklch(52% 0.103 154);
  --color-positive-ink: var(--color-accent);
  --color-highlight: oklch(95% 0.017 138);
  /* A shade off the canvas, with no meaning attached: a table header, a badge
     for a state that is neither good nor bad, a well. Distinct from
     --color-highlight, which says *this one* — the two were the same token and
     so every table header claimed to be selected. */
  --surface-quiet: oklch(96.5% 0.006 135);
  /* The prompt in an empty field, which has to sit below the ink it will be
     replaced by without disappearing. Every field in the app had its own copy
     of this mix before it was a token. */
  --color-ink-faint: color-mix(in oklab, var(--color-ink-soft) 60%, transparent);

  /* Tinted surfaces, so status colouring is decided once rather than per
     component.
     Mixed in oklab, and that is load bearing rather than a style choice. oklch
     interpolates hue as a number, and the canvas is oklch(100% 0 0) — hue 0 —
     so mixing any colour toward it dragged the result around the wheel toward
     red: --surface-positive resolved to hue 18 and shipped a salmon pill
     labelled LIVE, --surface-warning to hue 14, and --surface-sunk to hue 19,
     which is why every avatar and placeholder in the app was faintly pink.
     oklab has no hue channel to interpolate — a tint keeps the hue of the
     colour it is a tint of. */
  --surface-positive: var(--color-highlight);
  --surface-warning: color-mix(in oklab, var(--color-warning) 15%, var(--color-canvas));
  --surface-warning-strong: color-mix(in oklab, var(--color-warning) 20%, var(--color-canvas));
  --surface-negative: color-mix(in oklab, var(--color-negative) 8%, var(--color-canvas));
  /* Off the accent rather than off the ink. A sunk surface in this palette is
     sage — the comp's avatars and wells all are — and ink is now too close to
     neutral to tint anything. */
  --surface-sunk: color-mix(in oklab, var(--color-accent) 12%, var(--color-canvas));
  /* What a row does when you point at it. The faintest step off the canvas —
     the sage family, at a chroma low enough to read as a surface lighting up
     rather than as a colour.

     It lived on .sidebar as --sidebar-item-hover-background, with a note saying
     it graduates here the day a second list of rows wants it. An interactive
     table row is that second list, and pointing at something has to mean one
     thing everywhere in the app or the two drift into different greys.

     Deliberately not --surface-quiet, which is a step darker and says nothing —
     it is what a table header and a neutral badge are made of, and a hover the
     same weight as a permanent header is a row that always looks pointed at. */
  --surface-hover: oklch(97% 0.006 138);

  /* The channels we sell on, in their own colours.

     A brand mark is the one place in this app where a literal is the point:
     this is Airbnb's red rather than ours, so it is not on the palette above,
     it carries no meaning a tone would carry, and no theme overrides it — a
     logo that changed colour with the skin would stop being the logo. They are
     keyed by name rather than by channel code, because a code is a fact about
     the integration and a colour is a fact about the brand.

     Hex, in a file where every other colour is oklch, and deliberately: a value
     copied from a brand guide is auditable against it, where its conversion is
     something nobody can check. Close rather than certified — replace each one
     from the channel's own guide when the real artwork lands beside it. */
  --channel-airbnb: #ff5a5f;
  --channel-booking: #003580;
  --channel-vrbo: #1668e3;
  --channel-houfy: #00a0b0;

  /* Borders */
  --border-width: 1px;
  --border-width-thick: 1.5px;
  --border-width-heavy: 2px;
  --border: var(--border-width) solid var(--color-border);
  /* A rule between two rows of the same surface, rather than the edge of one.
     See --color-border-faint. */
  --border-faint: var(--border-width) solid var(--color-border-faint);
  --border-thick: var(--border-width-thick) solid var(--color-border);
  --border-dashed: var(--border-width-thick) dashed var(--color-border);
  --border-dotted: var(--border-width-heavy) dotted var(--color-border);
  --border-negative: var(--border-width) solid color-mix(in oklch, var(--color-negative) 30%, transparent);

  --radius-small: 0.5rem;
  --radius: 0.75rem;
  --radius-large: 1.25rem;
  --radius-pill: 99rem;
  --radius-circle: 50%;

  /* A card sits on the page; it does not float above it. The comp gives a panel
     a hairline and 4% at two pixels and nothing else, and the 24px halo this
     had made a list of panels read as a stack of loose cards. An overlay is the
     opposite case and keeps its lift — that shadow is how you know it is on
     top of something rather than in it.

     The hairline is the first of the three below and it is doing the comp's job
     rather than a shadow's: --color-bg is now the same white as the canvas, so
     the field no longer says where a card ends and this is the only thing that
     does. A spread-only ring rather than a border, because a border changes a
     panel's box and every flush table, sticky header and pinned column in the
     app is measured against that box. See the note on --color-bg — the two
     values are one decision. */
  --shadow-card:
    0 0 0 1px oklch(21% 0.008 250 / 7%),
    0 1px 2px oklch(21% 0.008 250 / 5%),
    0 4px 12px oklch(21% 0.008 250 / 4%);
  --shadow-overlay: 0 2px 8px oklch(21% 0.008 250 / 8%), 0 24px 64px oklch(21% 0.008 250 / 14%);

  /* Controls — buttons, inputs, and anything else the operator clicks or types
     into, so they stay the same height as each other by construction.

     These are em, not rem, and that is the whole trick: a control's padding,
     gap, and icon are measured against its own font-size, so a size variant
     sets font-size and everything inside follows. `.btn--small` is one
     declaration rather than five. */
  --control-padding-block: 0.6em;
  --control-padding-inline: 1.1em;
  --control-gap: 0.5em;
  /* The natural height of a control: its padding, its line box, and its border.
     Written as the formula rather than as a number so that an icon button stays
     square and in line with text buttons when the padding changes. aspect-ratio
     can't do this job — with both dimensions content-derived it has nothing
     definite to resolve against, and the button comes out glyph-width. */
  --control-size: calc(var(--control-padding-block) * 2 + var(--leading-normal) * 1em + var(--border-width) * 2);
  --control-size-small: 2em;
  --field-padding-block: 0.6em;
  --field-padding-inline: 0.75em;

  /* Panes on a flush screen. Both are here for the same reason --control-padding-*
     is: two blocks that must agree on a measurement, where neither can own it
     because neither contains the other.

     The height is the strip across the top of a pane, and all three panes wear it
     — the inbox's list header, the open conversation's, and the guest's — so the
     screen rules off on one line instead of reading as three windows pushed
     together.

     The gutter is the *open conversation's* inline padding, shared by the thread
     and by what we know about the guest, which are siblings rather than one inside
     the other. It is deliberately not the list's: a column of records reads denser
     than a column of prose, so the list stays on --space and only the reading side
     takes the step up. Do not read "pane" here as "every pane". */
  --pane-header-height: 5rem;

  /* The bottom strip of the app shell, and both halves of it take this rather
     than their own padding adding up to whatever it adds up to. The account
     sidebar's foot and a workspace rail's foot sit side by side at the bottom of
     the window with a rule along the top of each, and those two rules are read as
     one line — so a four-pixel difference in height is a line that steps.
     Neither component can see the other, which is the same reason
     --pane-header-height above is a token and not a number in one of the two
     files that need it. */
  --app-shell-footer-height: 4rem;
  --pane-gutter: var(--space-and-half);

  /* The app's one horizontal gutter: how far in from the edge of the window
     content starts. .main takes it as page padding, and .screen__toolbar,
     .screen__filters and .table--bleed take it so a full-bleed screen's title,
     its filters and the first column of its table all start on one line.

     Deliberately not --pane-gutter, which is a smaller thing wearing a similar
     name: that one is the *inside* padding of the open conversation's panes,
     and it is a step up from this because a column of prose reads wider than a
     column of records. This is the outside edge, and every screen has one.

     It grows with the viewport and then stops. A phone cannot spare it, and a
     wide screen has already had its line length capped by --main-width. */
  --screen-gutter: clamp(var(--space), 4vw, var(--space-double));

  /* Notices — callouts and validation summaries, which are the same box with
     different colouring. */
  --notice-padding: 0.9em 1.1em;

  /* Stacking. Named rather than numbered, so a new layer is a decision about
     what sits above what rather than a guess at a bigger number. */
  --z-sticky-cell: 2;
  --z-sticky-header: 3;
  /* The cell that is in both bands at once — a pinned first column's heading,
     which has to clear the rows sliding under it and the columns sliding past
     it. It is its own step rather than a header plus one, because "what sits
     above what" is a decision and arithmetic hides it. */
  --z-sticky-corner: 4;
  --z-popup: 10;
  --z-flash: 20;
  --z-dialog: 30;

  --duration-fast: 100ms;
  --duration: 200ms;
  --duration-slow: 320ms;
  --ease-out: ease-out;
  /* Decelerating curve for anything that travels a distance — a drawer coming
     in from an edge reads as arriving rather than snapping. */
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
  --transition-fast: var(--duration-fast) var(--ease-out);

  --opacity-muted: 0.35;
  --opacity-soft: 0.6;
  --opacity-disabled: 0.45;

  /* Three, and each one is a line length rather than a layout: a form reads
     narrow, a list of records needs its columns, and three panes need the room
     they need. */
  --main-width: 48rem;
  --main-width-roomy: 64rem;
  --main-width-wide: 76rem;

  /* Viewport bands.
   *
   * Redefining the two scales here resizes the whole UI at once — controls,
   * gutters, and text together — because every length downstream is a step on
   * one of them and every control measures its padding in em. That is the one
   * knob; there is no per-component breakpoint to keep in sync.
   *
   * It is deliberately not `html { font-size }`. Setting that overrides the
   * font size a person picked in their browser, which is the one number in the
   * page that isn't ours to take. rem stays anchored to their choice; we move
   * the scales that sit on top of it.
   *
   * Small screens get larger text and tighter gutters. The instinct is to
   * shrink everything on a phone, but a phone needs readable type and
   * thumb-sized controls more than it needs density, and it has no room to
   * spare between them. Density is what the desktop band is for.
   */
  @media (width < 40rem) {
    --space: 0.875rem;

    --text-xx-small: 0.75rem;
    --text-x-small: 0.8rem;
    --text-small: 0.875rem;
    --text-normal: 0.95rem;
    --text-body: 1.05rem;
    --text-medium: 1.15rem;
    --text-large: 1.4rem;
    --text-x-large: 1.75rem;
  }
}

@layer base {
  * {
    box-sizing: border-box;
  }

  /* A live subscription is a connection, not content. Turbo renders one as a real
     element, so left in the flow it becomes the first child of whatever holds it —
     which in a grid means an empty cell, and every pane after it lands one place
     to the right. That is exactly what happened the first time the inbox was
     subscribed from inside its own layout. */
  turbo-cable-stream-source {
    display: none;
  }

  /* Reserved whether or not the page scrolls, so opening a dialog — which
     locks scrolling — doesn't shift the page sideways underneath it. */
  html {
    scrollbar-gutter: stable;
  }

  body {
    margin: 0;
    font-family: var(--font-sans);
    /* Stated rather than inherited from the browser default, so the viewport
       bands above reach the em-sized controls that measure against it. */
    font-size: var(--text-body);
    color: var(--color-ink);
    background: var(--color-bg);
    line-height: var(--leading-normal);
  }

  h1, h2, h3 {
    font-weight: var(--weight-bold);
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
  }

  /* The one display size in the app, and the only place the heavy weight and
     the tighter tracking are used. A page has one title; everything below it is
     a heading inside content and stays on the text weights. */
  h1 {
    font-size: var(--text-x-large);
    font-weight: var(--weight-heavy);
    letter-spacing: var(--tracking-tighter);
    margin-block: 0 var(--space-quarter);
  }

  h2 {
    font-size: var(--text-medium);
  }

  a {
    color: inherit;
  }

  :focus-visible {
    outline: var(--border-width-heavy) solid var(--color-accent);
    outline-offset: var(--space-eighth);
  }

  /* Once, for every field there is. A component that restates it is a fourth
     copy of one colour decision. */
  ::placeholder {
    color: var(--color-ink-faint);
  }
}
