/* ============================================================================
   JOY GROUP — AGENT PORTAL, visual prototype.

   Its own stylesheet, NOT site.css. The portal is a tool, not a page of the
   marketing site: it needs a filter bar, a dense result list and an overlay,
   none of which exist there, and it needs none of the sections that do. It
   borrows the TOKENS (one import) and the .joy-btn family, and nothing else.

   Shell: minimal. Wordmark, the agent's name, a language switch, a sign-out.
   No drawer, no marketing footer. An agent signing in to check availability
   should not be handed a menu of company pages, and the login screen in
   particular has to read as quiet and serious rather than as a landing page.
   ========================================================================= */

/* NOTE: the spacing scale has no --space-7 (1,2,3,4,5,6,8,10,12,16,20,25).
   An undefined custom property makes the whole declaration invalid, so
   `padding: var(--space-7)` computed to 0 and the overlay's text ran to the
   panel edge. Check a token exists before reaching for it. */

/* ---------- the language switch ----------
   Restated here, not inherited: this file is loaded WITHOUT site.css, so the two
   rules that hide the language you are not reading have to come with it. They are
   a mechanism rather than a piece of the locked footer/drawer design, and the
   portal is a separate document — so this is the one place a copy is right.
   Same `joy-lang` key as the marketing site, so the choice carries across. */
html:not([data-lang="en"]) .L-en { display: none !important; }
html[data-lang="en"] .L-al { display: none !important; }

/* ---------- the focus ring is the keyboard's ----------
   A tap focuses nothing on a phone, so when the portal moves focus itself
   Safari takes it for the keyboard's and rings whatever it landed on: the
   owner saw a square round the loading circle, a ring round the unit's ✕,
   "and in other places" (2026-09-19). common.js puts `data-key` on <html>
   while the keyboard is the thing in use (Tab sets it, a touch or a click
   takes it off), and every focus rule in this file and in admin.css hangs off
   it — written `:where(html[data-key])`, which adds no weight, so each rule
   keeps the strength it had and the cascade below is untouched. The design
   system's own ring is restated here for the same reason. A keyboard keeps
   every indicator; a finger sees none. Text fields are not affected: their
   focus is an ink border (`:focus`, not `:focus-visible`). */
:focus-visible { outline: none; }
:where(html[data-key]) :focus-visible { outline: var(--focus-outline-width) solid var(--focus-ring);
  outline-offset: var(--focus-outline-offset); }

/* ---------- .joy-btn, ported ----------
   The CTA primitive lives in site.css, which this portal does not load — so the
   sign-out was an unstyled anchor: display block, no pill, and ink text sitting
   on the ink bar, which is to say invisible. This is the "minimal subset" the
   portal was always going to need: the tokens, the form field, and this.
   Ported, not re-designed — identical to site.css so the two never drift into
   two different buttons. The deliberate absence of a filled variant comes with
   it: on this site a filled pill IS the hover state, so one at rest reads as
   pressed. Order carries priority instead. */
.joy-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 13px 26px;
  font-family: var(--font-ui); font-weight: var(--fw-medium); font-size: 12px;
  letter-spacing: 0.14em; text-transform: uppercase; line-height: 1;
  background: transparent; border: var(--hairline) solid transparent;
  border-radius: 999px; text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background-color var(--dur-base) var(--ease-standard),
              color var(--dur-base) var(--ease-standard),
              border-color var(--dur-base) var(--ease-standard); }
.joy-btn[hidden] { display: none; }
.joy-btn--on-dark  { color: var(--crisp-white); border-color: var(--white-a62); }
.joy-btn--on-light { color: var(--ink-black);  border-color: var(--rule); }
.joy-btn__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 0; margin-left: 0; opacity: 0; overflow: hidden; font-size: 1.05em;
  transform: translateX(-6px);
  transition: width var(--dur-base) var(--ease-standard),
              margin-left var(--dur-base) var(--ease-standard),
              opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-standard); }
.joy-btn:hover, :where(html[data-key]) .joy-btn:focus-visible {
  background: var(--ink-black); color: var(--crisp-white); border-color: var(--ink-black);
  outline: none; }
.joy-btn:hover .joy-btn__icon, :where(html[data-key]) .joy-btn:focus-visible .joy-btn__icon {
  width: 0.95em; margin-left: 7px; opacity: 1; transform: translateX(0); }
.joy-btn--on-dark:hover, :where(html[data-key]) .joy-btn--on-dark:focus-visible {
  background: var(--crisp-white); color: var(--ink-black); border-color: var(--crisp-white); }

/* No anchor-underline reset needed on .joy-btn: its own `border:` shorthand already
   overrides the design system's `a { border-bottom }`, and a `border-bottom: 0`
   added "for safety" does real damage — it zeroes the BOTTOM of the pill, leaving
   an outline open at the base. .pt-bar__mark carries its own `border: 0`. */

/* ---------- shell ---------- */
/* A column, so the footer sits at the bottom of the VIEWPORT when the content is
   short and after the content when it is long. This replaces a guessed footer
   height in the login page's min-height — the guess was 96 against a real 86, so
   the login screen scrolled by 14px, which on a page holding two fields and a
   button reads as something being wrong. It also catches the list's empty state,
   where a filter matching nothing used to leave the footer riding up the screen. */
.pt-body { margin: 0; background: linear-gradient(var(--bg-page), var(--bg-page)) var(--ink-black);
  color: var(--text-primary); font-family: var(--font-ui);
  min-height: 100svh; display: flex; flex-direction: column; }
/* INK BEYOND THE PAGE (owner, 2026-09-19). Every page starts with the ink bar
   and ends with the ink footer, so whatever shows beyond the page is ink too:
   a pull past the top or the bottom (the rubber band), the space behind
   Safari's see-through bottom bar, a strip under the footer. It was white.
   Two colours decide it. The canvas is html's, so html is ink. What Safari and
   the iPhone app paint past the page's edges is the page's background COLOUR,
   html's with body's laid over it, and images left out; so the colour of
   .pt-body (the body) is ink as well, and its white is painted as an image,
   a flat gradient that covers its whole box. The page looks exactly as it
   did. Ink on html alone (the first try) left the pull white: the body's
   white colour lay over it. */
html { background: var(--ink-black); }
/* THE HOME-SCREEN APP FILLS THE SCREEN. In an installed iPhone app with
   viewport-fit=cover and the black-translucent status bar, iOS takes the
   status bar's height off 100%, 100svh and 100dvh, so the page stopped that
   much short of the bottom (a white band under the footer, 2026-09-19); 100vh
   is the whole screen there. Safari tabs keep 100svh: the height that fits one
   screen with the toolbars out. */
@media (display-mode: standalone) {
  .pt-body { min-height: 100vh; }
}
.pt-login { flex: 1 1 auto; min-height: 0; }
.pt-main { flex: 1 0 auto; }

.pt-bar { position: sticky; top: 0; z-index: 40; display: flex; align-items: center;
  justify-content: space-between; gap: var(--space-5);
  height: calc(72px + var(--sa-t)); padding: 0 clamp(20px, 3vw, 40px);
  padding-top: var(--sa-t); background: var(--ink-black); color: var(--crisp-white); }
/* THE PORTAL AS A HOME-SCREEN APP (owner's request, 2026-09-19). Opened from
   the icon, the page runs full screen under the phone's own status bar
   (apple-mobile-web-app-status-bar-style: black-translucent), and --sa-t is
   that bar's height. The ink bar reaches up behind the clock and GROWS by it:
   the bar is border-box, so a padding-top alone ate its 72 (60 on phones) and
   squeezed the wordmark and Sign out into what was left. Everywhere else --sa-t
   is 0 and nothing moves. The overlay, the password panel and the admin
   dialogs keep clear of the status bar and the home indicator the same way. */
/* flex: 0 0 auto, or the width above is a suggestion. The bar is a flex row and
   a flex item shrinks by default, so at 320 the sign-out — which is a pill, not
   the marketing header's small burger — squeezed the mark to 125px. The
   marketing header holds 168 at that width, and holding it is the whole point. */
.pt-bar__mark { flex: 0 0 auto;
  display: flex; align-items: center; gap: 14px; text-decoration: none; border: 0; }
/* THE WORDMARK IS THE MARKETING SITE'S, at the marketing site's size. It was
   sized by HEIGHT here — 20px, rendering 115.73 wide — against the 208px the
   header carries on every other page, so the portal wore the same mark at 56% of
   itself and read as a lesser thing than the site it belongs to.
   Sized by WIDTH now, exactly as #site-header .logo img is in site.css and as
   `logoW` is on the homepage: 208 above 768 and 168 below, which is one number
   in three places rather than a third opinion. At the mark's 5.7878 ratio that
   is 35.94px tall inside a 72px bar, and 29.03 inside the phone's 60px one. */
.pt-bar__mark img { width: 208px; height: auto; display: block; }
/* The word that says which of Joy Group's things this is. Set like the site's
   kickers, at the size the header can carry without competing with the mark. */
.pt-bar__where { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--white-a62); }
.pt-bar__right { display: flex; align-items: center; gap: var(--space-4); }
.pt-who { font-size: 13px; letter-spacing: 0.02em; color: var(--white-a62); }
/* The switch left the bar: this portal is for Albanian agents, and a language
   control is the last thing most of them will ever touch. It lives in the footer,
   and the footer is ink, so it is lit the way the header's type is. */
.pt-lang { display: inline-flex; align-items: center; gap: 6px; }
.pt-lang button { appearance: none; background: none; border: 0; cursor: pointer;
  padding: 4px 2px; font-family: var(--font-ui); font-size: 11px; font-weight: var(--fw-medium);
  letter-spacing: 0.12em; color: var(--white-a62); }
.pt-lang button[aria-pressed="true"] { color: var(--crisp-white); }
/* A RING, not a colour it already wears. This said `color: crisp-white` — which
   is exactly what the PRESSED button is at rest — while also removing the
   browser's own outline, so tabbing onto the language you are already reading
   changed nothing at all and took the default indicator away with it. */
:where(html[data-key]) .pt-lang button:focus-visible { outline: 1px solid var(--crisp-white);
  outline-offset: 3px; color: var(--crisp-white); }
.pt-lang > span { color: var(--white-a22); font-size: 11px; }
/* The phone's links line (see "the footer" in the phone block below, which
   must come after this): off on desktop. */
.pt-foot__links { display: none; }

/* Clipped, not hidden: `hidden` takes an element out of the accessibility tree
   as well as off the screen, which is the opposite of what a label for an icon
   button is for. */
.pt-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* ---------- the small footer, INK ----------
   White with a hairline over it, this was a line of small grey type under a
   white page: nothing about it said the page had ended, and on the login screen
   it read as a third paragraph. What ends a page is the ground changing — the
   marketing footer has always known that, and the bar at the top of this one
   already proves the portal can carry ink. So the footer is ink, and the
   hairline comes off with it: a tint edge does not need a rule to be an edge.
   The WhatsApp pill is the third thing in it and the reason it grew a right
   side — number, language, and the two of them kept apart from the address. */
.pt-foot { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3) var(--space-5); flex-wrap: wrap;
  /* 24, not 32. The pill is 38px tall where the type it replaced was 18, and at
     the old padding the footer went 86 -> 105 — which put the login page one
     pixel over a 900-tall screen and gave a page holding two fields a scrollbar.
     Tightening the padding by the difference puts the footer back at the 86 it
     has always been, so nothing downstream of it moved. */
  padding: var(--space-3) clamp(20px, 3vw, 40px) calc(var(--space-3) + var(--sa-b));
  background: var(--ink-black);
  font-size: 11px; letter-spacing: 0.04em; color: var(--white-a62); }
.pt-foot__side { display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2) var(--space-5); }
/* WhatsApp is a CIRCLE, and it is the only thing on the right. The number came
   off it: what an agent needs is the way to reach the desk, and the number was
   the widest element in a footer whose whole brief was "small and concise".
   40 on a mouse, 44 under a thumb — the smaller figure keeps the footer at the
   86px it has always been, which is what the login page's one-screen fit is
   measured against. It is lit and inverts like .joy-btn--on-dark, so the portal
   has one button language rather than a second one invented for the footer. */
.pt-wa { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: transparent; border: var(--hairline) solid var(--white-a62);
  color: var(--crisp-white); text-decoration: none;
  transition: background-color var(--dur-base) var(--ease-standard),
              color var(--dur-base) var(--ease-standard),
              border-color var(--dur-base) var(--ease-standard); }
.pt-wa svg { width: 19px; height: 19px; display: block; }
.pt-wa:hover, :where(html[data-key]) .pt-wa:focus-visible { outline: none;
  background: var(--crisp-white); color: var(--ink-black); border-color: var(--crisp-white); }
.pt-foot__who { letter-spacing: 0.16em; font-weight: var(--fw-medium); color: var(--crisp-white); }
.pt-foot__help a { color: var(--crisp-white); border-bottom-color: var(--white-a22); }
.pt-foot__help a:hover { color: var(--crisp-white); border-bottom-color: var(--white-a62); }

/* ---------- login ---------- */
/* space-8, not space-10. The card is 596 tall and the bar and footer take 158
   between them, which left 742 of a 900-tall screen against a 756 requirement -
   so a login page with two fields on it scrolled by fourteen pixels. 64 top and
   bottom is breathing room without being the reason the page does not fit. */
/* A PHOTOGRAPH BEHIND IT, and a scrim over that. The picture is one of the
   bright ones — white facades against a pale sky — so nothing laid on it has an
   edge of its own, and this design system does not allow a shadow to invent one.
   The scrim is neutral ink: it darkens without shifting a hue, which is the line
   between seating content and the cool duotone that was rejected for the
   marketing photography. Deeper on the phone, where the type sits straight on
   the picture instead of on a card.
   Two files, one per band — 2400px behind the desktop (448 KB), 1200px behind
   the phone (160 KB). Same discipline as Images/construction/: the camera master
   stays on disk, only the derivative ships. */
/* No --sa-b here or on .pt-main: the home indicator's room belongs to the
   footer, which closes every page. Both areas held it as well, and in the
   home-screen app that was 34px twice (2026-09-19). */
.pt-login { position: relative; display: grid; place-items: center;
  padding: var(--space-8) clamp(20px, 4vw, 40px) var(--space-8);
  background-image: url("/assets/agents/login.jpg");
  background-size: cover; background-position: center; }
.pt-login::before { content: ""; position: absolute; inset: 0;
  background: rgba(20, 20, 20, 0.45); }

/* DESKTOP: the original stack inside a white card, at the user's ask. The gaps
   between kicker, name and note are deliberately tight — 6, 16, 32 against the
   24, 32, 48 they used to be — because the card has to hold everything on one
   screen and that space was the cheapest to buy. */
.pt-login__card { position: relative; width: 100%; max-width: 420px;
  background: var(--crisp-white); border: var(--hairline) solid var(--rule);
  padding: var(--space-5) var(--space-4) var(--space-4); }
.pt-login__kicker { margin: 0 0 6px; font-size: 12px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: var(--fw-medium); color: var(--graphite); }
.pt-login__title { margin: 0 0 var(--space-2); font-family: var(--font-display);
  font-weight: var(--fw-display-light); font-size: var(--text-display-4-size);
  line-height: 1.1; letter-spacing: var(--text-display-4-ls); color: var(--text-primary); }
.pt-login__note { margin: 0 0 var(--space-4); max-width: 44ch;
  font-weight: var(--fw-light); font-size: 15px; line-height: 1.7; color: var(--text-secondary); }
.pt-form { display: flex; flex-direction: column; gap: var(--space-3); }
.pt-form label { display: block; }
.pt-form .pt-lbl { display: block; margin-bottom: 8px; font-size: 11px; letter-spacing: 0.18em;
  text-transform: uppercase; font-weight: var(--fw-medium); color: var(--graphite); }
.pt-form input { width: 100%; height: 52px; box-sizing: border-box;
  border: var(--hairline) solid var(--rule); border-radius: 2px; background: var(--crisp-white);
  padding: 0 16px; font-family: var(--font-ui); font-weight: var(--fw-light);
  font-size: 16px; color: var(--ink-black);
  transition: border-color var(--dur-base) var(--ease-standard); }
.pt-form input:focus { outline: none; border-color: var(--ink-black); }
.pt-form .joy-btn { margin-top: var(--space-3); justify-content: center; height: 52px; }
/* the eye at the end of a password field (common.js eye); the field keeps its
   own height, the button fills it, and Edge's own eye is put away so there are
   never two */
.pt-pw { position: relative; display: block; }
.pt-form .pt-pw input { padding-right: 52px; }
.pt-pw input::-ms-reveal, .pt-pw input::-ms-clear { display: none; }
.pt-pw__eye { position: absolute; top: 0; right: 0; bottom: 0; width: 48px; display: flex;
  align-items: center; justify-content: center; margin: 0; padding: 0; border: 0;
  background: transparent; color: var(--graphite); cursor: pointer; }
.pt-pw__eye:hover { color: var(--ink-black); }
:where(html[data-key]) .pt-pw__eye:focus-visible { outline: 1px solid var(--ink-black); outline-offset: -8px; color: var(--ink-black); }
.pt-pw__eye svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; }
.pt-login__apply { margin: var(--space-4) 0 0; padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--rule);
  font-size: 14px; line-height: 1.7; color: var(--text-secondary); }

/* SHORT LAPTOPS. The outer padding was solved for a 900-tall screen in ENGLISH;
   the Albanian note wraps to three lines, which makes the card 613 against 587,
   and 613 plus 2x64 of surround is 741 in the 740 a 900-tall screen leaves. So
   1440x900 was one pixel over and 1366x768 was 133 over, with a full-bleed
   photograph scrolling along with it.
   Gated at 920 rather than 860 so that 900 is inside it: the surround comes in,
   the card's own padding tightens, and both fit exactly. Above 920 nothing
   changes — a tall monitor keeps the roomier card.
   min-width: 769 because a phone has its own composition and scrolls by nature —
   this is about a laptop that is wide but short.
   PLACED AFTER .pt-login__apply, not before it: a media query adds no
   specificity, so an override written above the base rule it overrides simply
   loses the cascade. */
@media (min-width: 769px) and (max-height: 920px) {
  .pt-login { padding-top: var(--space-3);
    padding-bottom: var(--space-3); }
  /* .pt-login__card, not __body — the card was flattened when the phone stopped
     using one, and this rule was still naming a wrapper that no longer exists,
     so a third of the compression was doing nothing. */
  .pt-login__card { padding: var(--space-4); }
  .pt-login__note { margin-bottom: var(--space-3); }
  .pt-login__apply { margin-top: var(--space-3); padding-top: var(--space-2); }
  .pt-form { gap: var(--space-2); }
  .pt-form .joy-btn { margin-top: var(--space-2); }
}


.pt-login__apply a { color: var(--ink-black); }

/* ---------- filter bar ---------- */
.pt-main { padding: var(--space-8) clamp(20px, 3vw, 40px) var(--space-10); }
.pt-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-6); }
.pt-head h1 { margin: 0; font-family: var(--font-display); font-weight: var(--fw-display-light);
  font-size: var(--text-display-4-size); line-height: 1.1; letter-spacing: var(--text-display-4-ls); }
/* It heads the results column now, not the page title's right margin. 14px,
   because at 13 it was the smallest thing on the screen as well as the loneliest,
   and it is the only line that tells an agent a filter is hiding eight units. */
/* RIGHT, at every width. max-width has to come off with it: the design system
   caps every <p> at a reading measure, which lands this one at 538px inside a
   1196px column, so text-align alone would have stopped the figure a third of the
   way short of the edge it is meant to line up with. A two-word line has no
   measure worth protecting.
   On a phone the table is full bleed but the count is not — it stays inside the
   page's 20px gutter, so it ends exactly where the price column ends in every row
   beneath it (355 at 375 wide, 370 at 390, 410 at 430). Aligning it to the
   table's own edge instead would push it to the bezel and line it up with
   nothing. */
.pt-count { margin: 0 0 var(--space-3); font-size: 14px; letter-spacing: 0.02em;
  color: var(--graphite); max-width: none; text-align: right; }
.pt-results { min-width: 0; }
/* AND OUT OF THE COLUMN'S FLOW from 901, where the sidebar exists.
   In flow the count's line plus its margin was 47px of height at the top of the
   results column, so the table started at 398 while the filter panel started at
   351 — the two columns began on different edges, and once the count moved right
   the left half of that band was empty white. Taking it out of flow lets the
   table start where the panel does; the count sits in .pt-head's own 48px bottom
   margin instead, which was already empty.
   bottom: 100% puts its bottom edge on the column's top edge, and the 12px margin
   lifts it off: 13px clear of the header above, 12px clear of the table below,
   out of 25px of slack. It cannot collide however tall the header grows — the
   band is .pt-head's margin, and the count is measured from the column top, not
   from the header.
   Not below 901: there the filter is stacked ABOVE the results rather than beside
   them, so there is no second column top to line up with, and the count stays in
   flow where it reads as the caption to the run underneath it. */
@media (min-width: 901px) {
  .pt-results { position: relative; }
  .pt-count { position: absolute; right: 0; bottom: 100%; margin: 0 0 12px; }
}

/* ---------- the layout: filter left, list right ----------
   The filter is a COLUMN on desktop and it sticks, so it can be changed while
   the list scrolls past. `align-items: start` is what lets it stick — a default
   stretched grid item is as tall as the row and has nothing to stick within. The
   offset clears the sticky 72px bar. */
.pt-layout { display: grid; grid-template-columns: 300px 1fr;
  gap: var(--space-8); align-items: start; }
.pt-filters { position: sticky; top: calc(72px + var(--space-5) + var(--sa-t));
  display: grid; gap: var(--space-4); align-content: start;
  padding: var(--space-5); background: var(--bg-alt);
  /* A pinned panel taller than the space it is pinned in can never be scrolled
     into view — sticky does not move, so there is no scroll position that
     reveals its bottom. The panel is ~589px and pins at 112, so on anything
     under ~700px tall the Clear button was permanently below the fold. Capping
     it to the room available and letting it scroll inside itself is the only
     arrangement where every control stays reachable at every height. */
  max-height: calc(100svh - 72px - var(--space-5) - var(--sa-t) - var(--space-5));
  overflow-y: auto; }
.pt-f { display: flex; flex-direction: column; gap: 8px; }
.pt-f > span { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--graphite); }
/* :not([type="range"]) is load-bearing. The range inputs live inside a .pt-f, so
   this rule was giving them a 44px bordered white box and 14px of side padding -
   which pushed both thumbs off the 2px track they are supposed to ride and inset
   them from the ends of it. The number boxes below the track DO want this. */
/* A FIELD'S TEXT IS 16px UNDER A THUMB (owner, 2026-09-20: "when I tap on a
   text box there is a zoom in… when I exit the keyboard the page stays
   zoomed"). Safari on an iPhone zooms the whole page into any field whose
   text is under 16px, and leaving the keyboard does not zoom it back: the
   agent is left on a magnified page to pinch out by hand. 16px is the line
   Safari draws, so on a touch screen every field is 16px. One token carries
   it, so a field added later cannot miss it; the heights are fixed, so
   nothing moves, and 15 -> 16px is a fifth of a millimetre on the glass. The
   other way out is maximum-scale=1 in the viewport, which buys it by taking
   pinch-zoom away from everyone — not on a portal people read prices on. */
:root { --field-text: 15px; }
@media (pointer: coarse) { :root { --field-text: 16px; } }
.pt-f select, .pt-f input:not([type="range"]) { width: 100%; height: 44px; box-sizing: border-box;
  -webkit-appearance: none; appearance: none;
  border: var(--hairline) solid var(--rule); border-radius: 2px; background: var(--crisp-white);
  padding: 0 14px; font-family: var(--font-ui); font-weight: var(--fw-light);
  font-size: var(--field-text); color: var(--ink-black); }
/* the chevron CSS cannot draw on a native select, so it is painted on */
.pt-f select { background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-black) 50%),
    linear-gradient(135deg, var(--ink-black) 50%, transparent 50%);
  background-position: calc(100% - 18px) 19px, calc(100% - 13px) 19px;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 34px; }
.pt-f select:focus, .pt-f input:not([type="range"]):focus { outline: none; border-color: var(--ink-black); }
.pt-f--reset { justify-content: flex-end; }
.pt-f--reset button { height: 44px; appearance: none; cursor: pointer;
  background: none; border: var(--hairline) solid var(--rule); border-radius: 2px;
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: var(--fw-medium); color: var(--graphite);
  transition: color var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard); }
.pt-f--reset button:hover { color: var(--ink-black); border-color: var(--ink-black); }


/* ---------- the phone's filter toggle ----------
   Nothing above 768: there the filter is a sidebar with its own column. */
.pt-filter-toggle { display: none; }

/* ---------- the size range ----------
   Two <input type="range"> stacked on one track. Both are transparent and
   pointer-events: none so the painted track shows through and neither input
   swallows clicks meant for the other; only the THUMBS take the pointer back.
   Without that, whichever input is on top captures the whole width and the
   lower handle cannot be grabbed at all. */
.pt-range__wrap { position: relative; height: 26px; }
.pt-range__track { position: absolute; left: 0; right: 0; top: 12px; height: 2px;
  background: var(--soft-stone); }
.pt-range__fill { position: absolute; top: 0; height: 2px; background: var(--ink-black); }
.pt-range__in { position: absolute; left: 0; top: 0; width: 100%; height: 26px;
  margin: 0; padding: 0; background: transparent; pointer-events: none;
  -webkit-appearance: none; appearance: none; border: 0; }
.pt-range__in:focus { outline: none; }
.pt-range__in::-webkit-slider-thumb { -webkit-appearance: none; pointer-events: auto;
  width: 16px; height: 16px; border-radius: 50%; cursor: grab;
  background: var(--crisp-white); border: var(--hairline) solid var(--ink-black);
  box-shadow: 0 0 0 3px var(--bg-alt); }
.pt-range__in::-moz-range-thumb { pointer-events: auto;
  width: 16px; height: 16px; border-radius: 50%; cursor: grab; border: 1px solid var(--ink-black);
  background: var(--crisp-white); box-shadow: 0 0 0 3px var(--bg-alt); }
:where(html[data-key]) .pt-range__in:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--bg-alt), 0 0 0 5px var(--ink-black); }
.pt-range__nums { display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: end; gap: 8px; }
.pt-range__nums label { display: block; }
/* 11px, like every other small label here. 10 was the only one of its size in the
   whole portal and read as a mistake rather than a distinction. */
.pt-range__lbl { display: block; margin-bottom: 6px; font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: var(--fw-medium); color: var(--graphite); }
.pt-range__dash { padding-bottom: 13px; color: var(--graphite); }

/* ---------- the list ---------- */
/* A LIST, at the user's instruction, not a card grid: an agent is scanning for a
   fit against a client's brief, and rows compare where cards do not. The
   photography lives one click in, on the overlay. */
/* Two tables, not one list with headings in it. Each run is a boxed table with
   an ink head — the hairline goes right around the pair, at the user's ask, and
   the runs are set apart by white between the boxes rather than by a bigger gap
   inside one continuous rule. */
.pt-list { display: flex; flex-direction: column; gap: var(--space-8); }
.pt-row { display: grid; align-items: center; gap: var(--space-4);
  grid-template-columns: 1.5fr 1fr 1.4fr 1fr 24px;
  width: 100%; text-align: left; padding: var(--space-4) var(--space-3);
  background: none; border: 0; border-bottom: var(--hairline) solid var(--rule);
  cursor: pointer; font-family: var(--font-ui);
  transition: background var(--dur-fast) var(--ease-standard); }
.pt-row:hover, :where(html[data-key]) .pt-row:focus-visible { background: var(--bg-alt); outline: none; }
.pt-row__unit { font-size: 16px; font-weight: var(--fw-medium); color: var(--ink-black); }
/* 13px at 400, at EVERY width, phone included. The colour was never the problem
   — graphite is #6B6B6B, 5.3:1 on white, comfortably past AA — it is the stroke:
   Neue Haas Light at 12px is genuinely faint. Size and weight are the two levers
   and both move a little rather than either being pushed far.
   NOT a tag. The project line was briefly 11px uppercase ink at 500 on the phone,
   and the user's verdict was that bold made it indistinguishable: set like that
   it stopped reading as the unit's subtitle and started competing with the unit's
   own name above it. Bigger, not louder — and one treatment at every width
   rather than two ideas about the same line. */
.pt-row__sub { display: block; margin-top: 3px; font-size: 13px; font-weight: var(--fw-regular);
  letter-spacing: 0.02em; color: var(--graphite); }
.pt-cell { font-size: 14px; font-weight: var(--fw-light); color: var(--text-secondary); }
/* A tag, not a sentence. At 14px light it was the same voice as the sub-line two
   inches to its left, which already says the same word — so the eye read the type
   twice and neither time as a category. Uppercase and tracked, it is scannable
   down the column, which is the only reason to give a repeated word its own
   column at all. */
.pt-cell--type { font-size: 11px; font-weight: var(--fw-medium); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--graphite); }
/* Total size for an apartment, villa size for a villa — the headline figure the
   user named as the one that matters, with the breakdown under it rather than
   instead of it. */
.pt-size__lead { font-size: 16px; font-weight: var(--fw-medium); color: var(--ink-black); }
.pt-size__parts { display: block; margin-top: 3px; font-size: 13px; font-weight: var(--fw-regular);
  color: var(--graphite); }
/* SALE OR RENT, against the price. It used to be the last word of the grey
   sub-line — 12px, the same grey as everything around it, in the place a reader's
   eye has already left. Whether a unit is sold or let is the second thing an
   agent needs to know after what it costs, so it is said where they are already
   looking, as a tag rather than as more sentence. The tint is what makes it a
   tag; on a hovered row the row itself goes mist, so the tag goes white to stay
   one. */
/* The bedroom count, in the Albanian convention the client already thinks in.
   Set as the deal tag is, so the row carries one tag on each side — what you get
   on the left, how you get it on the right — rather than two unrelated
   treatments. Same inversion on hover, for the same reason. */
.pt-beds { display: inline-block; margin-left: 10px; padding: 3px 8px;
  background: var(--bg-alt); vertical-align: 1px;
  font-size: 11px; letter-spacing: 0.06em; font-weight: var(--fw-medium);
  color: var(--ink-black); white-space: nowrap; }
.pt-row:hover .pt-beds, :where(html[data-key]) .pt-row:focus-visible .pt-beds { background: var(--crisp-white); }

.pt-price { display: flex; flex-direction: column; align-items: flex-end; gap: 7px;
  text-align: right; }
.pt-price__v { font-size: 16px; font-weight: var(--fw-medium); color: var(--ink-black); }
.pt-deal { display: inline-block; padding: 3px 8px; background: var(--bg-alt);
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--ink-black); white-space: nowrap; }
.pt-row:hover .pt-deal, :where(html[data-key]) .pt-row:focus-visible .pt-deal { background: var(--crisp-white); }
/* Sale and rent told apart at a glance (owner's request, 2026-09-19), in the
   brand's own two tones rather than an accent hue: a sale is the ink tag, a
   rent stays the mist one. The ink tag keeps its ground on the mist row: the
   doubled class outranks the hover and busy rules, one of which comes later. */
.pt-deal.pt-deal--sale,
.pt-row:hover .pt-deal.pt-deal--sale, :where(html[data-key]) .pt-row:focus-visible .pt-deal.pt-deal--sale,
.pt-row[aria-busy="true"] .pt-deal.pt-deal--sale { background: var(--ink-black); color: var(--crisp-white); }
.pt-chev { justify-self: end; color: var(--soft-stone); font-size: 15px; }
.pt-row:hover .pt-chev { color: var(--ink-black); }

/* ---------- the overlay ---------- */
.pt-ov[hidden] { display: none !important; }
.pt-ov { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  padding: calc(clamp(16px, 3vw, 40px) + var(--sa-t)) clamp(16px, 3vw, 40px)
    calc(clamp(16px, 3vw, 40px) + var(--sa-b)); }
.pt-ov__scrim { position: absolute; inset: 0; background: rgba(20,20,20,0.62); border: 0; }
/* Opening a unit puts focus on the dialog itself (portal.js open()), which has
   nothing to outline: the ring the keyboard's outline drew round the close
   button on the iPhone is gone with it (owner, 2026-09-19). */
.pt-ov:focus { outline: none; }
/* The TEXT scrolls, not the panel. A grid item stretches to the row, and the row
   is as tall as the taller column — so once the price terms made the body column
   taller than the screen, the media column stretched with it and everything
   pinned to the bottom of the image stage (the floor caption, the dots) sat below
   the panel's own bottom edge, off screen. Capping the panel and letting the body
   scroll inside it fixes that and is better besides: the image stays put while
   the specification is read beside it. min-height: 0 on both, or a grid item
   refuses to shrink below its content and the overflow never engages. */
.pt-ov__panel { position: relative; width: min(1040px, 100%); max-height: calc(90svh - var(--sa-t) - var(--sa-b)); overflow: hidden;
  background: var(--crisp-white); display: grid; grid-template-columns: 1.25fr 1fr;
  /* minmax(0, 1fr), not the implicit auto row. max-height caps the CONTAINER, but
     an auto row still sizes to its content and overflows straight back out of it -
     which is why min-height: 0 on the children changed nothing on its own. */
  grid-template-rows: minmax(0, 1fr); }
/* On this screen the two columns are the panel's own grid items: .pt-ov__scroll
   is display: contents and puts them there. It becomes a real box, the one
   thing that scrolls, only on a phone (see "THE PHOTO GOES WITH THE DETAILS"). */
.pt-ov__scroll { display: contents; }
.pt-ov__scroll > .pt-ov__media { min-height: 0; }
.pt-ov__body { min-height: 0; overflow-y: auto; }
.pt-ov__media { background: var(--bg-alt); min-height: 320px; display: flex; flex-direction: column; }

/* Two tabs, not a strip of numbered thumbnails. "Photo 1 / Photo 2" made the
   reader count; "Images / Floor plan" tells them what they are choosing between. */
.pt-tabs { display: flex; gap: var(--space-5); padding: var(--space-4) var(--space-5) 0; }
.pt-tab { appearance: none; background: none; border: 0; cursor: pointer; padding: 0 0 10px;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--graphite);
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard); }
.pt-tab[aria-pressed="true"] { color: var(--ink-black); border-bottom-color: var(--ink-black); }
/* This painted --bg-alt on a ground that IS --bg-alt: .pt-ov__media carries the
   same token, so the focused tab and its 6px halo measured 1.00:1 against what
   was already behind them — invisible — and on the SELECTED tab, which is ink at
   rest, the colour change was a no-op too. An ink ring is the one thing that
   shows on this ground, and it cannot be confused with the underline that means
   selected. (Predates this session; found in the sweep and fixed here.) */
:where(html[data-key]) .pt-tab:focus-visible { outline: 1px solid var(--ink-black); outline-offset: 3px;
  color: var(--ink-black); }

/* A SWIPE ACROSS THE PICTURE IS SIDEWAYS ONLY (owner, 2026-09-20: "when I
   swipe left and right I happen to swipe unintentionally up and down the
   page"). A thumb never travels in a straight line, and the phone was taking
   the up-and-down part of the stroke as a scroll while the portal took the
   sideways part as a step, so changing a photograph dragged the sheet with it.
   touch-action tells the phone up front which way this box may be dragged:
   sideways, which is the carousel's. It has to be the phone's decision — a
   touchmove handler cannot take a scroll back once iOS has begun it. The
   second line hands pinch-zoom back where the browser knows the word; one that
   does not drops the line and keeps the first. Scrolling the sheet is the rest
   of the panel's job, which is every part of it but this box. */
.pt-stage { position: relative; flex: 1; min-height: 280px; overflow: hidden;
  touch-action: pan-x; touch-action: pan-x pinch-zoom; }
/* CONTAIN, never cover. The photography is landscape and the floor plans all
   are, and the desktop stage measured 578x753 — a ratio of 0.77. A 3:2 render at
   `cover` shows 51% of its width and the rest is thrown away; a cropped floor
   plan is not a floor plan at all. Nothing here may be cropped.
   The ground goes from soft-stone to mist so the surround reads as a mount round
   the image rather than a slab behind it. */
/* display: BLOCK, not grid. As a grid with place-items:center the row was auto
   sized by the image, so the img's own `height: 100%` had no definite row to
   resolve against and fell back to auto — a 1400x990 plan came out 578x409
   inside a 385-tall stage and lost 24px off the top and bottom to the stage's
   overflow:hidden. `contain` never got the chance to engage. Absolutely
   positioned with inset:0 the frame's height IS definite, so the image fills it
   and contain letterboxes inside it, which is the whole point. */
.pt-frame { position: absolute; inset: 0; background: var(--bg-alt); }
.pt-frame img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ON the margins, as asked — vertically centred, one at each edge of the stage.
   White on a ring so they hold against a photograph of any brightness; there is
   no photography yet and they still must not disappear against the first one. */
.pt-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 40px; height: 40px; display: grid; place-items: center; cursor: pointer;
  background: rgba(255, 255, 255, 0.92); border: var(--hairline) solid var(--rule);
  border-radius: 50%; font-size: 15px; line-height: 1; color: var(--ink-black);
  transition: background var(--dur-fast) var(--ease-standard); }
.pt-arrow:hover { background: var(--crisp-white); }
/* Focus INVERTS rather than brightening. The hover state takes a 92%-white disc to
   100% white, which as a focus indicator is invisible — the one state a keyboard
   user has no other way of finding. Inverting matches the house idiom, where
   .joy-btn's focus is its hover state filled. */
:where(html[data-key]) .pt-arrow:focus-visible { outline: none;
  background: var(--ink-black); color: var(--crisp-white); border-color: var(--ink-black); }
.pt-arrow--prev { left: var(--space-3); }
.pt-arrow--next { right: var(--space-3); }
.pt-arrow[hidden] { display: none; }

/* The floor's NAME, because three empty frames with three dots under them do not
   say which floor you are looking at — and until there is imagery, the name is
   the only thing that does. Sits above the dots, centred with them. */
.pt-caption { position: absolute; left: 0; right: 0; bottom: 28px; z-index: 2;
  margin: 0; text-align: center;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--ink-black); }
.pt-caption:empty { display: none; }

.pt-dots { position: absolute; left: 0; right: 0; bottom: 12px; z-index: 2;
  display: flex; justify-content: center; gap: 6px; }
.pt-dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(30,30,30,0.28); }
.pt-dots i.is-on { background: var(--ink-black); }
.pt-ov__body { padding: var(--space-6); }
.pt-ov__close { position: absolute; top: 10px; right: 10px; z-index: 2; width: 36px; height: 36px;
  cursor: pointer; background: var(--crisp-white); border: var(--hairline) solid var(--rule);
  border-radius: 50%; font-size: 15px; line-height: 1; color: var(--ink-black); }
/* The unit's name is the first thing read and the thing an agent says on the
   phone, so it stops being the same Light the whole site whispers in. Begum
   ships Regular, Medium, Semibold and Bold as real files here — this is a
   genuine weight, not a synthesised smear. Medium rather than Bold: at 28px
   against 11px tracked labels, Bold shouts, and the point is presence, not
   volume. 600 and 700 exist if more is wanted. */
.pt-ov__title { margin: 0 0 2px; font-family: var(--font-display);
  font-weight: var(--fw-medium); font-size: var(--text-display-5-size); line-height: 1.2; }
.pt-ov__where { margin: 0 0 var(--space-5); font-size: 13px; letter-spacing: 0.02em; color: var(--graphite); }
.pt-specs { margin: 0 0 var(--space-6); border-top: var(--hairline) solid var(--rule); }
.pt-spec { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4);
  padding: 12px 0; border-bottom: var(--hairline) solid var(--rule); }
.pt-spec dt { font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--graphite); }
.pt-spec dd { margin: 0; font-size: 15px; font-weight: var(--fw-light); color: var(--ink-black); }
/* The one figure the user singled out as the most important gets the weight. */
.pt-spec--lead dd { font-size: 20px; font-weight: var(--fw-medium); }
.pt-ov__cta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
/* The mark sits BEFORE the label and is always visible, unlike .joy-btn__icon,
   which is a hover affordance that lives at width 0 at rest — an icon that
   identifies the destination has to be there before you hover. */
.pt-ov__wa-mark { flex: 0 0 auto; width: 15px; height: 15px; margin-right: 9px;
  display: block; }
.pt-ov__note { flex: 1 1 100%; margin: 0; font-size: 12px; font-weight: var(--fw-light);
  line-height: 1.6; color: var(--graphite); }
/* An anchor with no href is already unclickable and unfocusable; this is what
   makes that visible rather than a button that merely does nothing. */
.joy-btn.is-off { opacity: 0.4; cursor: default; pointer-events: none; }
.pt-ov__note:empty { display: none; }


/* ---------- small helps for a first-time reader ---------- */
.pt-hint { margin: var(--space-2) 0 0; max-width: 56ch;
  font-size: 14px; font-weight: var(--fw-light); line-height: 1.6; color: var(--text-secondary); }

/* "Showing 4 of 11" rather than a bare count: a number on its own does not tell
   someone that a filter is hiding the rest. */
.pt-count strong { font-weight: var(--fw-medium); color: var(--ink-black); }

/* The clear button goes quiet when there is nothing to clear, so the one moment
   it matters it is the only live control in the panel. */
.pt-f--reset button[disabled] { opacity: 0.38; cursor: default; }
.pt-f--reset button[disabled]:hover { color: var(--graphite); border-color: var(--rule); }

.pt-empty { padding: var(--space-10) 0; text-align: center; color: var(--graphite); font-size: 15px; }
.pt-empty .joy-btn { margin-top: var(--space-4); }

/* ---------- what the price includes ---------- */
.pt-terms { margin: 0 0 var(--space-6); }
.pt-terms h3 { margin: 0 0 var(--space-2); font-family: var(--font-ui);
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--graphite); }
.pt-terms ul { margin: 0 0 var(--space-4); padding: 0; list-style: none; }
.pt-terms li { position: relative; padding: 3px 0 3px 20px;
  font-size: 14px; font-weight: var(--fw-light); line-height: 1.55; color: var(--text-primary); }
/* A tick and a cross drawn as text, so the two lists are told apart at a glance
   without colour — this palette has no green and no red to spend. */
.pt-terms li::before { position: absolute; left: 0; top: 3px; font-size: 13px; }
.pt-terms .is-in  li::before { content: "+"; color: var(--ink-black); }
.pt-terms .is-out li::before { content: "\2013"; color: var(--graphite); }
.pt-terms .is-out li { color: var(--text-secondary); }
.pt-terms__none { margin: 0 0 var(--space-4); font-size: 14px; font-weight: var(--fw-light);
  line-height: 1.55; color: var(--graphite); }
.pt-terms__ask { margin: 0; padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--rule);
  font-size: 12px; line-height: 1.6; color: var(--graphite); }

/* A word on how the apartment figures relate, because "net", "common" and
   "total" only look self-evident to someone who already knows. */
.pt-specs__note { margin: calc(var(--space-2) * -1) 0 var(--space-5);
  font-size: 12px; font-weight: var(--fw-light); color: var(--graphite); }

/* ---------- the standing notice, and its dismiss ---------- */
.pt-demo { display: flex; align-items: flex-start; gap: var(--space-3);
  padding: 12px clamp(20px, 3vw, 40px); background: var(--soft-stone);
  font-size: 12px; line-height: 1.6; letter-spacing: 0.04em; color: var(--ink-black); }
/* The X goes to the band's right edge, not to the end of the sentence: the
   design system caps every <p> at a reading measure, so a paragraph in a 1640px
   band stops at 461 and a button that merely followed it landed in the middle of
   the screen. `margin-left: auto` puts it where a dismiss belongs whatever the
   text does. The measure is widened a little here — this is a 12px notice, not
   body copy, and 461px of it was two lines against 1100px of empty band. */
.pt-demo__t { margin: 0; flex: 1 1 auto; max-width: 96ch; }
/* Pulled out to the band's own edges with negative margins, so the target is 32px
   without the band growing to hold it. */
.pt-demo__x { flex: 0 0 auto; appearance: none; background: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; margin: -5px -8px -5px auto;
  font-size: 12px; line-height: 1; color: var(--ink-black); opacity: 0.65;
  transition: opacity var(--dur-fast) var(--ease-standard),
              background-color var(--dur-fast) var(--ease-standard); }
.pt-demo__x:hover { opacity: 1; }
:where(html[data-key]) .pt-demo__x:focus-visible { outline: none; opacity: 1; background: var(--crisp-white); }
html.notice-off .pt-demo { display: none; }

/* ---------- the type groups ---------- */
/* Each run is a TABLE with a head, not a heading over a list. The head is ink and
   the box is a hairline all the way round, both at the user's ask — and the two
   together are what make the end of the villas and the start of the apartments
   impossible to scroll past, which was the other half of it.
   The name stays Begum, now in capitals: the display face is what the rest of
   this site uses to say "new section", and the row's own type cell goes the other
   way into a small tracked tag, so the two never read as two sizes of one thing.
   The count is no longer a label — it is a number, and an agent scanning for how
   much is on offer reads it before the word next to it. */
.pt-grp { border: var(--hairline) solid var(--rule); background: var(--crisp-white); }
.pt-group { display: flex; align-items: center; gap: var(--space-3);
  width: 100%; margin: 0; padding: 15px var(--space-3);
  background: var(--ink-black); color: var(--crisp-white);
  border: 0; text-align: left; font-family: var(--font-ui); }
.pt-group__name { font-family: var(--font-display); font-weight: var(--fw-display-light);
  font-size: calc(var(--text-display-5-size) * 0.72); line-height: 1.15;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--crisp-white); }
/* (5), and the brackets are CSS so the number in the DOM stays a number.
   Vertically the two do not centre by themselves: the name is Begum caps at
   line-height 1.15 and the count is Neue Haas with parentheses, and each box
   centres its own line rather than its own ink. The shift below is measured —
   see the note where it is set. */
.pt-group__n { font-size: 18px; font-weight: var(--fw-light); letter-spacing: 0.02em;
  color: var(--white-a62); line-height: 1; }
.pt-group__n::before { content: "("; }
.pt-group__n::after  { content: ")"; }
/* THE SHIFT. align-items centres each span's LINE BOX; what has to line up is the
   ink, and neither face puts its ink in the middle of its own box.

   MEASURE THE BASELINE, DO NOT DERIVE IT. The first attempt at this solved the
   baseline from canvas fontBoundingBoxAscent/Descent and was wrong by 0.84px: at
   20.16px Chrome reports those as a rounded 19 and 7, and the metrics it actually
   LAYS BEGUM OUT WITH are different again. A zero-height inline-block with
   vertical-align:baseline aligns its top to the real baseline and reports it to
   three decimals — that is the only figure to trust here.
   Probed at 1440x900, both group heads, after document.fonts.ready:
     name  Begum 20.16px / line-height 23.184 -> baseline 17.000px from box top
           (0.84325em, NOT the 0.885em the font metrics imply). Ink 13.810 above
           it and 0.222 below, so the caps' centre rides 1.380px = 0.0682em HIGH
           of the box centre.
     count Haas 18px / line-height 18 -> baseline 15.000. "(5)" inks 13.572 above
           and 2.124 below, so the brackets' centre rides 0.276px = 0.0153em LOW.
   Up by the sum, 1.656px, and written against the token because the caps half
   scales with the name (0.0682 x 0.72 = 0.0491 per token px) while the brackets
   half is fixed at the count's own 18px.
   --text-display-5-size is a FLAT 28px on this page — the 769+ clamp lives in
   site.css:1780 and portal.css does not load site.css — so this computes to the
   same -1.65px at every width. It is written as the token anyway so the shift
   follows if the name's size is ever changed.
   Verified after: residual 0.01px, overshoot 0.83 above the cap band and 0.83
   below it. Before, it was 0.726px low and hung 1.56px under the baseline against
   0.11px over the cap top — a 14.7x imbalance. */
.pt-group__n { position: relative;
  top: calc(var(--text-display-5-size) * -0.0491 - 0.276px); }
/* The box supplies the line under the last row; the row's own would double it. */
.pt-rows .pt-row:last-child { border-bottom: 0; }

/* ---------- the phone's filter pills ----------
   Hidden here and shown in the phone block, not the other way round: the sidebar
   shows its own state at every other width, so a second copy of it there would be
   the same information said twice. */
.pt-chips { display: none; }
.pt-chip { appearance: none; cursor: pointer; display: inline-flex; align-items: center;
  gap: 10px; min-height: 44px; padding: 0 12px 0 14px;
  background: var(--bg-alt); border: 0;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; font-weight: var(--fw-medium); color: var(--ink-black);
  transition: background-color var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard); }
.pt-chip__x { font-size: 11px; color: var(--graphite); }
.pt-chip:hover, :where(html[data-key]) .pt-chip:focus-visible { outline: none;
  background: var(--ink-black); color: var(--crisp-white); }
.pt-chip:hover .pt-chip__x, :where(html[data-key]) .pt-chip:focus-visible .pt-chip__x { color: var(--crisp-white); }

/* ---------- the stage is landscape where the card is two columns ----------
   The media column is a grid item and stretched to the panel, which the BODY
   makes tall — 810px at 1440x900 — so the stage came out 578x753 and portrait.
   `contain` alone would fix the cropping and leave the dots and the floor
   caption stranded 184px below the image they belong to, because both are
   positioned against the stage's bottom rather than the picture's.
   So the stage takes a landscape ratio of its own width and centres itself in
   the column. The leftover is the media column's own mist, which is the same
   tone as the frame's, so the column still reads as one panel with the image
   mounted in it — and the arrows, the caption and the dots all sit against the
   picture's edges again.
   Only where the card HAS two columns. At 900 and below it stacks, the media
   takes its own row, and the stage is already 343x240 — landscape, which is why
   the phone was reported as fine. */
@media (min-width: 901px) {
  .pt-stage { flex: 0 0 auto; aspect-ratio: 3 / 2; margin: auto 0; }
}

/* ---------- narrow desktop / landscape tablet, 901-1200 ----------
   At 1024 the sidebar left the list 598px for five columns: the unit cell was
   127px and the size cell 118, and every row wrapped to three lines. The band is
   above 900 so it keeps the sidebar - a stacked filter would waste the width -
   but the sidebar gives back 40px and the type column comes off, exactly as it
   does on phones and for the same reason: the run is already headed with it. */
@media (min-width: 901px) and (max-width: 1200px) {
  .pt-layout { grid-template-columns: 260px 1fr; gap: var(--space-6); }
  .pt-row { grid-template-columns: 1.7fr 1.4fr 1fr 24px; }
  .pt-cell--type { display: none; }
}

/* ---------- tablet and phone ---------- */
@media (max-width: 900px) {
  /* THE PHOTO GOES WITH THE DETAILS (owner, 2026-09-20: "when you scroll
     through details the photo stays put and it's occupying around 50% of the
     screen"). Stacked, the picture used to be pinned above a body that
     scrolled under it, so half the screen was the same photograph however far
     down the specification the agent read. Now ONE scroller holds both —
     .pt-ov__scroll, a real box here, display: contents above 900 — and the
     photo moves up out of the way, leaving the whole panel to the figures.
     The close button is not in it: it stays absolute on the panel, which is
     what an earlier attempt at scrolling the panel itself got wrong (at 390
     the ✕ was gone after ~98px and never came back). The picture keeps its
     own height (.pt-stage's 240px), so swiping it sideways still steps the
     photos; the swipe only counts when it is more sideways than up. */
  .pt-ov__panel { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); overflow: hidden; }
  .pt-ov__scroll { display: block; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  .pt-ov__body { overflow-y: visible; }
  /* A 300px sidebar at 900 leaves the list 560px, which the five columns cannot
     hold. The filter goes back above the list and stops sticking - a panel that
     deep pinned to the top of a short screen would cover the results it filters. */
  .pt-layout { grid-template-columns: 1fr; gap: var(--space-6); }
  /* THREE columns, named, not auto-fit. At 820 auto-fit gave four tracks: the
     three selects and Clear filled row 1, the range took the whole of row 2, and
     Clear was pushed alone onto row 3 beside 482px of empty mist — while the two
     number boxes, stretched across a full-width range, reached 333px each (371 at
     900) for a field that holds three digits. Fixed at three, the range takes two
     of them and Clear sits in the third, bottom-aligned by the align-items
     already here, and the boxes come back to 213. */
  .pt-filters { position: static; grid-template-columns: repeat(3, 1fr);
    align-items: end; }
  .pt-range { grid-column: 1 / 3; }
}
/* A thumb is already on the image, so the arrows come off and the stage is
   swiped. Gated on POINTER TYPE, not width — a touch tablet at 900 swipes too,
   and a narrow desktop window still has a mouse and keeps its arrows. */
@media (hover: none) {
  .pt-arrow { display: none; }
}

@media (max-width: 768px) {
  /* ---- the filter folds away ---- */
  .pt-filter-toggle { display: flex; align-items: center; justify-content: space-between;
    width: 100%; min-height: 48px; padding: 0 var(--space-4); cursor: pointer;
    background: var(--bg-alt); border: 0;
    font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.16em;
    text-transform: uppercase; font-weight: var(--fw-medium); color: var(--ink-black); }
  .pt-filter-toggle__n { font-size: 11px; letter-spacing: 0.04em; color: var(--graphite); }
  /* The chevron turns rather than swapping glyphs, so the control reads as one
     thing in two states instead of two controls. */
  .pt-filter-toggle__n::after { content: "\25BE"; display: inline-block; margin-left: 10px;
    font-size: 13px; letter-spacing: 0; color: var(--ink-black);
    transition: transform var(--dur-fast) var(--ease-standard); }
  .pt-filter-toggle[aria-expanded="true"] .pt-filter-toggle__n::after { transform: rotate(180deg); }
  .pt-filters { display: none; }
  .pt-layout.is-open .pt-filters { display: grid; }
  /* ---- and what it set stays on screen while it is folded ----
     A filter you cannot see is a filter you forget you set. The count on the bar
     says how many; these say which, and each X drops one of them without the
     panel being opened at all. */
  .pt-chips { display: flex; flex-wrap: wrap; gap: 8px; }
  .pt-chips:empty { display: none; }
  /* The pills are a grid item of .pt-layout like the bar and the list, so the
     layout's own 48px row gap was what sat above and below them — a row of pills
     floating in the middle of the page, reading as belonging to neither. Tighten
     the whole column instead of fighting the gap with a negative margin: on a
     phone the bar, the panel, the pills and the list are one apparatus. */
  .pt-layout { row-gap: var(--space-3); }

  /* ---- the runs retract ----
     Phone only. On a 390px screen eleven rows is four and a half screens, and an
     agent who has found their villa still has to scroll the whole apartment run
     to reach the bottom of the page. Folding a run is how they stop having to. */
  .pt-group { padding: 12px var(--space-2); cursor: pointer; }
  .pt-group__chev { margin-left: auto; display: inline-flex; align-items: center;
    font-size: 13px; color: var(--white-a62); }
  .pt-group__chev::after { content: "\25BE"; display: inline-block;
    transition: transform var(--dur-base) var(--ease-standard); }
  .pt-grp.is-shut .pt-group__chev::after { transform: rotate(-90deg); }
  .pt-grp.is-shut .pt-rows { display: none; }
  /* A finger's target on the notice, and the band does not grow to hold it. */
  .pt-demo__x { width: 44px; height: 44px; margin: -9px -12px -9px auto; }
  /* ---- NO PHOTOGRAPH ON THE PHONE, at the user's instruction ----
     The page stays the white it always was, so there is no scrim to fight, no
     white type, and no card: this is the original composition exactly, and the
     only thing done to it is taking out enough vertical slack that it holds one
     screen. The picture is a desktop treatment.
     That also means the 1200px derivative is never fetched on a phone, which is
     160 KB a phone no longer downloads. */
  .pt-login { background-image: none;
    padding-top: var(--space-3);
    padding-bottom: var(--space-3); }
  .pt-login::before { display: none; }
  .pt-login__card { background: none; border: 0; padding: 0; }
  .pt-login__apply { margin-top: var(--space-3); padding-top: 0; border-top: 0; }
  /* the phone's bar is 60, not 72 */
  .pt-form input { scroll-margin-top: calc(60px + var(--sa-t) + var(--space-2)); }

  /* ---- full bleed ----
     The tables run to the edges of the screen. Inset by the page's 20px gutter
     they sat as two floating cards in a white surround, and on a 390px phone
     that gutter is 10% of the width spent on framing a table that is already
     framed by its own hairline. Edge to edge, the ink head is a band across the
     screen the way a section is, and the rows get their gutter back INSIDE the
     box, so the type still lines up with the heading above.
     The side borders go with the inset — a hairline against the bezel is not a
     box, it is a line you cannot see the other side of. */
  .pt-grp { margin-left: -20px; margin-right: -20px;
    border-left: 0; border-right: 0; }
  .pt-group { padding-left: 20px; padding-right: 20px; }

  /* ---- the footer: ONE line, or TWO where a screen has links (owner, 2026-09-19) ----
     The company's name, the AL / EN pair and WhatsApp: the "questions?"
     address is the desktop's. (For an afternoon the pair was one button naming
     the language shown; the owner brought the pair back once the footer had
     room for it.) display: contents on the side dissolves the wrapper so
     its children are the footer's own items: in a flex row the name takes the
     room and the rest sits at the right edge, whether or not the page has
     WhatsApp.
     THIN: 16px above and below, not 24, and the 44px targets (the language
     buttons, WhatsApp) keep their size for a finger but take it from the space
     around them (-13px above and below), so they no longer set the line's
     height. The admin footer went from 116px to about 80, the sign-in's from
     88 to about 50. */
  .pt-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px var(--space-3);
    padding-top: var(--space-2); padding-bottom: calc(var(--space-2) + var(--sa-b)); }
  .pt-foot__side { display: contents; }
  .pt-foot__who { order: 1; margin-right: auto; }
  .pt-foot__help { display: none; }
  .pt-foot .pt-lang { order: 2; }
  .pt-foot .pt-wa { order: 3; }
  .pt-foot .pt-lang button, .pt-foot .pt-wa { margin-top: -13px; margin-bottom: -13px; }
  /* A SCREEN WITH LINKS is two lines. THE ADMIN SCREEN, in the owner's
     quarters: the name top left, "Pamja e agjentëve" top right, the language
     bottom left, "change password" bottom right. THE LISTINGS (.pt-foot--wa),
     as the owner laid them out after: "Pamja admin" (admins only) right after
     the name, "change password" right after the language, WhatsApp bottom
     right. The two links share a column, so they line up. (owner, 2026-09-19) */
  .pt-foot--links { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px var(--space-3); }
  .pt-foot--links .pt-foot__links { display: contents; }
  .pt-foot--links .pt-foot__who { grid-area: 1 / 1; margin: 0; }
  .pt-foot--links .pt-lang { grid-area: 2 / 1; justify-self: start; margin-left: -10px; }
  .pt-foot--links .pa-footlink { grid-area: 1 / 2; justify-self: end; }
  .pt-foot--links [data-change-password] { grid-area: 2 / 2; justify-self: end; }
  .pt-foot--wa { grid-template-columns: auto 1fr auto; }
  .pt-foot--wa [data-admin-link] { grid-area: 1 / 2; justify-self: start; }
  .pt-foot--wa [data-change-password] { grid-area: 2 / 2; justify-self: start; }
  .pt-foot--wa .pt-wa { grid-area: 2 / 3; justify-self: end; margin-left: -10px; margin-right: -10px; }
  .pt-login__ask { display: none; }   /* "Nuk keni akses? / No access yet?" */
  /* NO RING on the phone. At 44px around a 19px glyph the ring read as a button
     floating free of everything near it; the glyph alone sits in the bar instead
     of on top of it. The box stays 44px — that is the finger's target, not the
     mark's size — and the ring comes back on focus, where an outline is the
     whole point. */
  .pt-wa { width: 44px; height: 44px; justify-self: end; align-self: center;
    border: 0; background: none; }
  .pt-wa svg { width: 24px; height: 24px; }
  .pt-wa:hover { background: none; color: var(--crisp-white); }
  :where(html[data-key]) .pt-wa:focus-visible { background: var(--crisp-white); color: var(--ink-black); }

  /* ---- and the sign-out narrows ----
     121px in English against a 168px wordmark, in a 60px bar. The FONT is
     untouched at the user's instruction; it is .joy-btn's 26px of side padding
     that made the pill, and 16 is still a comfortable pill. 121 -> 101 in
     English, 77 -> 57 in Albanian. The height stays 44: that is the touch floor,
     not decoration. */
  .pt-bar .joy-btn { padding-left: 16px; padding-right: 16px; }

  .pt-bar { height: calc(60px + var(--sa-t)); padding: 0 20px; padding-top: var(--sa-t); }
  /* THE PICTURE HAS THE PANEL'S WIDTH TO WORK WITH (owner, 2026-09-20: "the
     photo is slightly small now, increase size"). Now that it scrolls away
     with the details it can take the room: 4:3 of the panel instead of a flat
     240px — 398 x 298 on the owner's phone, a quarter taller — so a landscape
     photograph fills the panel from edge to edge instead of standing in mist
     bars. The 240 stays as the floor for the narrowest screens. */
  .pt-stage { min-height: 240px; aspect-ratio: 4 / 3; }
  /* The two words sit level with the ✕ beside them (their middle on its
     middle, 28px down the panel), and their underline comes in close: at 10px
     it read as a rule under the row rather than a mark under the word. */
  .pt-tabs { padding-top: 21px; }
  .pt-tabs .pt-tab { padding-bottom: 6px; }
  .pt-bar__mark img { width: 168px; }   /* the phone figure, same as site.css */
  .pt-who { display: none; }          /* the name is not worth a line on a phone */
  /* "Agent portal" comes off too: at 390 it wrapped to two lines inside a 60px
     bar and squeezed the sign-out to a 35px sliver of itself. Once an agent is
     signed in, the bar does not need to tell them where they are. */
  .pt-bar__where { display: none; }
  .pt-bar__right { gap: var(--space-3); }
  /* 44px is the floor for a finger. Sign out measured 40 and the footer's language
     buttons 21 — both were comfortably under it. The language pair grows by
     padding, so the text stays the size it was and only the target changes. */
  /* 36, not 44. The pill sat 8px off the top and bottom of a 60px bar and read
     as filling it; at 36 it clears 12 each side and the bar has an edge again.
     Vertical padding has to go with the minimum — .joy-btn's own 13px top and
     bottom makes 38 on its own, so min-height alone would have changed nothing —
     and box-sizing pins the border inside the figure rather than adding 2 to it.
     The label is untouched at 12px, and 11px of ink still surrounds it.
     This is BELOW the 44px target this file uses everywhere else, at the user's
     instruction and knowingly: 44 is Apple's recommendation and WCAG 2.5.5 AAA,
     while the AA requirement (2.5.8) is 24 and 36 clears it by half again. The
     other phone targets — the group heads, the pills, the notice's X, the
     language switch — all stay at 44. */
  .pt-bar .joy-btn { flex: 0 0 auto; white-space: nowrap;
    box-sizing: border-box; min-height: 36px; padding-top: 0; padding-bottom: 0; }
  .pt-foot .pt-lang button { display: inline-flex; align-items: center;
    min-height: 44px; padding: 0 10px; }
  /* The range thumbs sit in a 26px band, which is a small thing to catch with a
     thumb. The band grows on touch; the track and the thumbs do not move, because
     the track is centred in it. */
  .pt-range__wrap { height: 44px; }
  .pt-range__track { top: 21px; }
  .pt-range__in { height: 44px; }
  .pt-main { padding: var(--space-6) 20px var(--space-8); }
  .pt-filters { grid-template-columns: 1fr 1fr; padding: var(--space-4); }
  .pt-range { grid-column: 1 / -1; }
  /* PROJECT gets the full width. In a half-column at 390 the select is 130.8px
     with about 80.8px of text area, and both project names are far longer than
     that — "Joy Residences — The Villas" measures 168px and "The Apartments" 209,
     so a closed select read "Joy Residenc" whichever one was chosen. There is no
     ellipsis to fall back on: text-overflow computes to clip inside a select. The
     control has to be wide enough to say which project is selected, because
     saying so is the whole of its job.
     :first-child, NOT :first-of-type — two children are .pt-f (a label and the
     range's div), so :first-of-type would match both and stretch the range too. */
  .pt-filters > .pt-f:first-child { grid-column: 1 / -1; }
  /* The row stops being columns and becomes a block: five columns on a 390px
     screen gives every one of them about 70px, which no price fits. */
  /* 20px, the page's own gutter, so a full-bleed table's type sits on the same
     left margin as the heading and the hint above it. */
  .pt-row { grid-template-columns: 1fr auto; gap: var(--space-2) var(--space-3);
    padding: var(--space-4) 20px; }
  .pt-row__unit { grid-column: 1; }
  .pt-price { grid-column: 2; grid-row: 1; }
  /* The type is in the run's heading. (.pt-cell--deal was listed here and never
     existed - the row has no deal cell.) */
  .pt-cell--type { display: none; }
  .pt-size { grid-column: 1 / -1; }
  .pt-chev { display: none; }
  .pt-ov__body { padding: var(--space-5); }
}


/* ---------- SHORT PHONES: the login still has to be one screen ----------
   375x812 and 430x932 have room; 375x667 did not — 60 of bar, 544 of login and
   114 of footer is 718 against 667, so it was 51 over. Every number below is
   taken off the FORM's own rhythm rather than off the words: inputs and button
   46 instead of 52, the gaps and the label offsets in by a notch. Nothing is
   hidden at this size and nothing is smaller to read.
   46 is still clear of the 44px touch floor — that is the point of stopping
   there rather than at 40. */
@media (max-height: 750px), (display-mode: standalone) and (max-width: 768px) and (max-height: 840px) {
  .pt-login .pt-form { gap: var(--space-2); }
  .pt-login .pt-form input { height: 46px; }
  .pt-login .pt-form .joy-btn { height: 46px; margin-top: var(--space-2); }
  .pt-login .pt-form .pt-lbl { margin-bottom: 6px; }
  .pt-login__note { margin-bottom: var(--space-3); }
  .pt-login__apply { margin-top: var(--space-2); }
}

/* SHORTER LAPTOPS, 830px tall and under (the owner's choice "b", 2026-09-19).
   The approved "activity in the portal is recorded" line costs 23.8px, and on
   top of the 12-30px the prototype already scrolled here it made 1366x768 and
   1024x768 scroll 35px, 1280x720 53px and 1280x800 3px, with the photograph
   scrolling along. This step takes it back from SPACING only — the white round
   the card, the card's own padding, two gaps, and the small apply text set at
   1.4 as it already is on phones. Fields, button and type keep their sizes.
   830 so that 864 and 900 tall stay exactly as they were. Desktop only:
   min-width 769. PLACED AFTER the short-phone block (max-height: 750px), which
   has no width limit and so reaches desktops under 750 too; written with the
   same selectors, so that at equal specificity the later rule wins. */
@media (min-width: 769px) and (max-height: 830px) {
  .pt-login { padding-top: var(--space-2);
    padding-bottom: var(--space-2); }
  .pt-login__card { padding: var(--space-3); }
  .pt-login__note { margin-bottom: var(--space-2); }
  .pt-login__apply { margin-top: var(--space-2); line-height: 1.4; }
}
/* and at 740 and under (1280x720), the gaps between the fields and before the
   button close from 16 to 8 as well.
   Measured, both languages, widths 1024-1920: every height from 700 to 1080
   fits; 657 (a 1366x768 screen once the browser's own bars are taken off)
   scrolls 40px, where the prototype scrolled more. Fitting that would mean
   smaller fields or a smaller title, which "tighten the spacing" did not ask. */
@media (min-width: 769px) and (max-height: 740px) {
  .pt-login .pt-form { gap: var(--space-1); }
  .pt-login .pt-form .joy-btn { margin-top: var(--space-1); }
}

/* Shorter still — 320x568 is an iPhone SE 1, and after the band above it is
   394px of room against 487 of content. The note is the one element that can go:
   it describes what the portal is, which the heading directly above it already
   says, and the alternative is the scrolling the user ruled out. The heading
   drops a tier with it. Everything that has a JOB on this screen — both fields,
   the button, and the line telling you how to get access — stays. */
/* LANDSCAPE SCROLLS, at the user's instruction, and that is the right call.
   A two-column arrangement did fit 844x390 — words beside the form, the two
   fields on one row — but it made a second layout to maintain for a posture
   nobody signs in from twice, and it read as a different product from the same
   phone held upright. A landscape phone is 844-932 CSS pixels wide, which is to
   say indistinguishable from a small laptop window, and CLAUDE.md already
   settles that case: it takes the desktop composition. So it takes the desktop
   composition and scrolls.
   What scrolling needs is somewhere to land. The bar is sticky, so a field the
   browser scrolls into view on focus — which is what every mobile browser does
   when the keyboard comes up — would otherwise stop directly underneath it.
   scroll-margin-top reserves the bar's height plus a little air, so the field
   arrives clear of it with its label visible. */
.pt-form input { scroll-margin-top: calc(72px + var(--sa-t) + var(--space-2)); }

@media (max-width: 768px) and (max-height: 620px) {
  .pt-login__note { display: none; }
  .pt-login__title { font-size: var(--text-display-5-size); margin-bottom: var(--space-2); }
  .pt-login { padding-top: var(--space-2);
    padding-bottom: var(--space-2); }
}

/* ---------- the live portal: what real sign-in and real names need ----------
   Added when the prototype was connected to the server, and nothing else: no
   new colour, no new rule, no shadow. */

/* Why sign-in failed. Said in words and in weight rather than colour: this
   palette has no red to spend, and status is carried by wording. EMPTY it has
   no height and no margin, so the signed-off form rhythm is untouched until it
   has something to say. */
.pt-form__err { margin: 0; max-width: none; font-size: 14px; line-height: 1.5;
  font-weight: var(--fw-medium); color: var(--ink-black); }
.pt-form__err:not(:empty) { margin-top: var(--space-3); }
/* Sign in, pressed and waiting on the server, says so to the pointer as well. */
.joy-btn[aria-busy="true"] { cursor: progress; }
/* "Activity in the portal is recorded." RUNS ON from the apply sentence (owner,
   2026-09-20: "let the activity is recorded sentence go next to apply and we
   will get back to you"). It was a line of its own under it. The space between
   the two is the line break in the markup, which is why the span stays: it
   holds the Albanian and the English of one sentence inside a paragraph that
   already holds another. It keeps inheriting the apply text's small style
   rather than restating it. */
.pt-login__rec { display: inline; }
/* ...and the height it costs is paid back on SHORT PHONES, so the sign-in page
   still holds one screen there. The line is 23.8px (14px at 1.7), and 375x667
   and 320x568 had fitted to the pixel without it — both scrolled by 17 and 13
   once it was in. Paid from the two places with slack rather than from the form:
   the white above and below the block goes down one token step, and the small
   apply text is set at 1.4 instead of 1.7 — its three short lines are a footnote,
   not body copy. The inputs keep their 46px and the gaps their tokens.
   ONLY WHERE THE PAGE WOULD OTHERWISE SCROLL. Both bands are gated on the
   tallest screen that needs them, so every taller phone keeps the prototype's
   rhythm untouched. Measured in Chrome, both languages, every height from 540
   to 800 at widths 320-768, with the compression taken out:
     - the first step is needed up to 726px tall (365 wide, Albanian — the
       title and note wrap below 370); 703 at 375-430 wide, 710 at 480-768.
       Gated at 730: 360x740, 412x732 and 414x736 fit without it and are back to
       the prototype's rhythm (375x720 is inside the band, for 360-365 wide AL).
     - the second is needed up to 593px tall (480-768 wide; 585 at 430,
       567 at 320). Gated at 600.
   The few pixels over each figure are headroom for another engine's text
   metrics, not a measured need.
   After, document height against the screen, AL / EN:
     320x568 568 / 568 · 375x667 667 / 667 · 390x664 664 / 664
     390x844 and 430x932 are outside both bands and unchanged.
   360x640 still scrolls (697 AL, 653 EN), as the prototype already did (699,
   655): the Albanian title and note wrap there. That is its own question.
   The first step also runs from 751 to 790px tall. The prototype's own form
   compression (max-height: 750px, above) stops at 750, so from 751 up nothing
   paid for the new line and 751-785 scrolled by up to 27px where the prototype
   had fitted; 731-750 fit without it because that compression still applies. */
@media (max-width: 768px) and (max-height: 730px), (display-mode: standalone) and (max-width: 768px) and (max-height: 840px) {
  .pt-login { padding-top: var(--space-2);
    padding-bottom: var(--space-2); }
  .pt-login__apply { line-height: 1.4; }
}
@media (max-width: 768px) and (min-height: 751px) and (max-height: 790px), (display-mode: standalone) and (max-width: 768px) and (min-height: 841px) and (max-height: 880px) {
  .pt-login { padding-top: var(--space-2);
    padding-bottom: var(--space-2); }
  .pt-login__apply { line-height: 1.4; }
}
@media (max-width: 768px) and (max-height: 600px) {
  .pt-login { padding-top: var(--space-1);
    padding-bottom: var(--space-1); }
}

/* THE NAME IN THE BAR IS REAL NOW, and as long as an admin types it: up to 60
   characters of name and 80 of agency. The bar is one line of fixed height, so
   the NAME gives way — cut with an ellipsis, whole on hover through its title —
   and the pills beside it never do. Without this a long agency pushed the
   sign-out off the right edge. */
.pt-bar__right { min-width: 0; }
.pt-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE CROSS-LINK BETWEEN THE TWO SCREENS — "Pamja admin / Admin" on the
   listings, "Pamja e agjentëve / Agent view" on the admin — one quiet device
   for both, not a second pill: two pills side by side read as two equal
   choices, and Sign out is the one that ends the session. Tracked capitals in
   the bar's own dimmed white, with the design system's link hairline under
   them. Defined here because both screens load portal.css (the admin screen
   used to carry it alone, as .ad-barlink).
   `hidden` has to win: the listings keep the link hidden from agents. */
.pt-barlink { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--white-a62); white-space: nowrap;
  border-bottom: var(--hairline) solid var(--white-a22); padding: 2px 0; }
.pt-barlink:hover, :where(html[data-key]) .pt-barlink:focus-visible { color: var(--crisp-white);
  border-bottom-color: var(--white-a62); }
.pt-barlink[hidden] { display: none; }
/* Not in a phone's bar: it has room for the wordmark and Sign out and no more.
   Measured with the admin link showing as a pill, Sign out ran past the right
   edge in English at every phone width (23px at 430, 70 at 375) and in
   Albanian from 390 down. An admin who signs in lands on the panel anyway;
   the admin screen offers its way back in its footer. Agents never see the
   listings' link at any width. */
@media (max-width: 768px) {
  .pt-barlink { display: none; }
}

/* A row pressed and waiting on /api/unit keeps the look it has under the
   pointer — the mist row, the tags turned white so they stay tags, the arrow
   in ink — after the pointer has left, and shows the wait to the pointer. The
   overlay opens when the answer arrives. */
.pt-row[aria-busy="true"] { background: var(--bg-alt); cursor: progress; }
.pt-row[aria-busy="true"] .pt-deal,
.pt-row[aria-busy="true"] .pt-beds { background: var(--crisp-white); }
.pt-row[aria-busy="true"] .pt-chev { color: var(--ink-black); }

/* The overlay's error state takes the Images / Floor plan tabs away: there is
   nothing behind either. .pt-tabs is display:flex, which beats the browser's
   own [hidden] rule, so it is said here. */
.pt-tabs[hidden] { display: none; }

/* ---------- below the calibrated range ----------
   The phone scale is solved at 430x932 and 375x667, so 375 is this project's
   narrow anchor — and at 375 the bar holds the full wordmark in BOTH languages:
   20 + 168 + 33.6 + 121.36 + 20 = 362.96 against 375.
   At 320 Albanian still fits (measured on a clean load: mark 168, "DIL" 76.91,
   35.1px between them, document 320 wide, no overflow). English does not:
   "SIGN OUT" is 121.36 against "DIL"'s 76.91, and the bar would need 362.96 of a
   320px screen. Rather than scroll the whole PAGE sideways to hold a fixed width,
   flex-shrink is handed back below 375. It only fires on genuine overflow, so
   Albanian keeps its 168 at 320 and only English gives anything up.
   This block goes LAST, after the 768 block closes — an earlier version of it was
   opened in the middle of that block and swallowed everything below the wordmark
   rule, which took the phone's row layout and header with it. */
@media (max-width: 374px) {
  .pt-bar__mark { flex-shrink: 1; }
}

/* ---------- change your own password (both screens) ----------
   The link: in the bar beside the name on computers and tablets (.pt-barlink,
   which the phone block already hides), and on a phone as the footer's last
   line instead, the way the admin screen moves its "Agent view" link there.
   The panel: a native <dialog>, so focus stays inside it and Esc closes it,
   dressed exactly as the sign-in card: same white, same edge, same fields,
   same button. No shadow; the page behind is dimmed the way the sign-in
   photograph is. */
.pt-footlink { display: none; }
.pt-dlg { width: min(420px, calc(100vw - 32px)); max-height: calc(100svh - 32px - var(--sa-t) - var(--sa-b));
  top: var(--sa-t); bottom: var(--sa-b);          /* centred between the status bar and the home indicator */
  box-sizing: border-box; overflow: auto; margin: auto;
  padding: var(--space-5) var(--space-4) var(--space-4);
  background: var(--crisp-white); color: var(--ink-black);
  border: var(--hairline) solid var(--rule); border-radius: 0; }
.pt-dlg::backdrop { background: rgba(20, 20, 20, 0.45); }
/* .pt-form is display:flex, which beats the browser's own [hidden] rule; the
   panel swaps the form for the "changed" message by hiding one of them. */
.pt-dlg [hidden] { display: none; }
/* an empty error line would still take a gap in the flex column */
.pt-dlg .pt-form__err:empty { display: none; }
.pt-dlg .pt-login__title { margin-bottom: var(--space-1); }
.pt-dlg .pt-login__note { margin-bottom: var(--space-3); }
.pt-dlg__actions { display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-3); }
.pt-dlg__actions .joy-btn { margin-top: 0; flex: 1 1 auto; }
.pt-dlg__cancel { flex: 0 0 auto; padding: 12px 0; background: none; border: 0;
  border-bottom: var(--hairline) solid var(--rule); cursor: pointer;
  font: inherit; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: var(--fw-medium); color: var(--text-secondary, #6B6B6B); }
.pt-dlg__cancel:hover, :where(html[data-key]) .pt-dlg__cancel:focus-visible { outline: none; color: var(--ink-black); border-bottom-color: var(--ink-black); }
.pt-dlg__done { margin: 0 0 var(--space-3); font-size: 15px; line-height: 1.6; }
/* The new password's rules (owner's rule, 2026-09-19), ticked off while the
   person types (common.js): a small dot and a line each, the dot going from
   soft stone to ink and the words from graphite to ink as a rule is met. No
   boxes, no colour. */
.pt-pwrules { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 10px 0 0; padding: 0;
  list-style: none; font-size: 12px; line-height: 1.4; color: var(--graphite); }
.pt-pwrules li { display: flex; align-items: center; gap: 6px; }
.pt-pwrules__dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: var(--soft-stone);
  transition: background-color var(--dur-fast, 120ms) var(--ease-standard, ease); }
.pt-pwrules li.is-met { color: var(--ink-black); }
.pt-pwrules li.is-met .pt-pwrules__dot { background: var(--ink-black); }
@media (max-width: 768px) {
  /* The link sits on the footer's links line (see "the footer: ONE line"). */
  .pt-footlink { display: inline;
    font-size: 11px; letter-spacing: 0.04em; color: var(--crisp-white);
    border-bottom: var(--hairline) solid var(--white-a22); }
  .pt-footlink[hidden] { display: none; }
  .pt-footlink:hover, :where(html[data-key]) .pt-footlink:focus-visible { outline: none; border-bottom-color: var(--white-a62); }
}

/* ---------- the tick rows: "keep me signed in on this device", "I'm not a robot" ----------
   Plain rows under the password: the browser's own checkbox (keyboard, screen
   reader and touch all come free), drawn in ink with accent-color so it stays
   monochrome. The pair sits tight under the field, one 8px step apart, and the
   button's own top margin gives way to them, so the card grows by the rows and
   little else. While the captcha is still being worked out, a ticked robot box
   shows the browser's in-between dash (indeterminate, portal.js) until it is done.
   Selectors carry .pt-login so they outrank `.pt-form input` (52px, full width)
   and the short-screen `.pt-login .pt-form input { height: 46px }`. */
.pt-login .pt-form .pt-ticks { display: flex; flex-direction: column; gap: var(--space-1);
  margin-top: calc(var(--space-1) * -1); }
.pt-login .pt-form .pt-tick { display: flex; align-items: center; gap: 10px; cursor: pointer;
  font-size: 13px; line-height: 1.4; letter-spacing: 0.01em; color: var(--text-secondary); }
.pt-login .pt-form .pt-tick input { flex: 0 0 auto; width: 18px; height: 18px; margin: 0;
  padding: 0; border: 0; accent-color: var(--ink-black); cursor: pointer; }
.pt-login .pt-form .pt-tick input:indeterminate { cursor: progress; }
:where(html[data-key]) .pt-login .pt-form .pt-tick input:focus-visible { outline: 1px solid var(--ink-black); outline-offset: 3px; }
.pt-login .pt-form .pt-ticks + .joy-btn { margin-top: 0; }
/* With Cloudflare Turnstile on (lib/captcha.js), the robot row is Cloudflare's:
   the row is hidden ([hidden] must beat its display:flex) and the widget's own
   box takes its place, seen only when Cloudflare wants a click. Its flexible
   size is at least 300px wide: on the narrowest phones (320px, 280px of
   content) it borrows 10px of the side margins either way to fit. */
.pt-login .pt-form .pt-tick[hidden] { display: none; }
.pt-login .pt-form .pt-turnstile { min-width: 0; }
@media (max-width: 340px) {
  .pt-login .pt-form .pt-turnstile { margin-left: -10px; margin-right: -10px; }
}
/* PAYING FOR THE CHECKBOX ROW (about 10px) where the sign-in page has to fit
   one screen: one spacing step off the gap under the note and the gap above
   the apply text, on phones up to 790 tall and on desktops up to 740 tall.
   Measured against the page as it was before the row, both languages: no size
   scrolls more than it did. Last in the file on purpose: every block these
   override sits above, and a media query adds no specificity. */
@media (max-width: 768px) and (max-height: 790px), (display-mode: standalone) and (max-width: 768px) and (max-height: 880px) {
  .pt-login__note { margin-bottom: var(--space-2); }
  .pt-login__apply { margin-top: var(--space-1); }
}
@media (min-width: 769px) and (max-height: 740px) {
  .pt-login__note { margin-bottom: var(--space-1); }
}
/* At 620 tall and under the note is gone, so the saving above cannot come from
   it: the gap under the title (which the form now follows) closes a step. */
@media (max-width: 768px) and (max-height: 620px) {
  .pt-login__title { margin-bottom: var(--space-1); }
}

/* PAYING FOR THE ROBOT ROW (the captcha, 2026-09-19): 26.2px at every size,
   the 18.2px row and the 8px step above it. Taken back band by band so that
   no size scrolls more than it did before the row. Measured in headless
   Chrome, both languages, every height from 480 to 1000 at widths 320-768 and
   from 560 to 1100 on desktop, against the page as it was.
   - THE MARGIN ROUND THE BLOCK comes first, on both platforms: the photograph
     round the card on desktop is 16px at every height (it was 64 above 920
     and 24 from 831), the white above and below the block on a phone 8px
     (it was 21 or 14). It only ever shows where the page is tight: wherever
     there is height to spare the block is centred in it, as before.
   - DESKTOP, 830 tall and under: the card's own padding goes from 24 to 16,
     and the gap and rule above the apply text from 16 to 8 each. At 740 and
     under the title's gap closes to 8 as well, and pays for the tick rows to
     sit 8px under the password field instead of touching it (the form's gap
     is 8 there, and the rows' -8 pulled them onto the field's edge).
   - PHONES: above 790 tall the note's gap under it comes down from the
     space-4 step to the form's own space-3 (it paid the last 0.8px at 360
     wide in Albanian, where the title takes two lines); from 751 to 790 tall
     the note's gap closes to 8; at 730 and under the title's, the note's and
     the form's gaps are all 8, with the tick rows 8px under the field. At 600 and under, where the note has already gone
     and every gap is 8, no spacing is left to pay with: the "Hyrje / Sign in"
     kicker goes too. The title above the form and the Sign in button below
     it both say what it said.
   Title, note and kicker are named inside .pt-login: the change-password
   panel (common.js) borrows those three classes and keeps its own spacing.
   The first-admin page (setup/) shares the sign-in block, as it shares
   every band above.
   The page as it was scrolled from 921 to 949 tall on desktop (it needed 950,
   above the 920 band's reach); with the 16px margin it fits there now. */
@media (min-width: 769px) {
  .pt-login { padding-top: var(--space-2);
    padding-bottom: var(--space-2); }
}
@media (min-width: 769px) and (max-height: 830px) {
  .pt-login__card { padding: var(--space-2); }
  .pt-login__apply { margin-top: var(--space-1); padding-top: var(--space-1); }
}
@media (min-width: 769px) and (max-height: 740px) {
  .pt-login .pt-login__title { margin-bottom: var(--space-1); }
  .pt-login .pt-form .pt-ticks { margin-top: 0; }
}
@media (max-width: 768px) {
  .pt-login { padding-top: var(--space-1);
    padding-bottom: var(--space-1); }
}
@media (max-width: 768px) and (min-height: 791px) {
  .pt-login .pt-login__note { margin-bottom: var(--space-3); }
}
@media (max-width: 768px) and (min-height: 751px) and (max-height: 790px), (display-mode: standalone) and (max-width: 768px) and (min-height: 841px) and (max-height: 880px) {
  .pt-login .pt-login__note { margin-bottom: var(--space-1); }
}
@media (max-width: 768px) and (max-height: 730px), (display-mode: standalone) and (max-width: 768px) and (max-height: 840px) {
  .pt-login .pt-login__title, .pt-login .pt-login__note { margin-bottom: var(--space-1); }
  .pt-login .pt-form { gap: var(--space-1); }
  .pt-login .pt-form .pt-ticks { margin-top: 0; }
}
@media (max-width: 768px) and (max-height: 600px) {
  .pt-login .pt-login__kicker { display: none; }
}

/* THE HOME-SCREEN APP ON A PHONE (2026-09-19). Opened from the icon the page
   is the whole screen, and the status bar and the home indicator take 81 to
   96px of it (20 on an SE), so the bands above, measured in Safari, would pick
   the spacing for a taller screen than the page really has. Each band's query
   therefore also names the app's heights, shifted by what the two bars take:
   up to 840 tall (the minis, 812) is spaced as up to 730; 841 to 880 (844,
   852, 874) as 751 to 790; 881 and up (the Plus and Pro Max sizes) as above
   790. The SE, 667 tall with a 20px status bar and no home indicator, is still
   one pixel over after that: its white above and below the form, 8px each,
   gives way (the form is centred in what is left, 7.5px either side).
   Measured in headless Chrome with each iPhone's safe areas, both languages.
   On Android the app sits below the system bars, so its height is already
   what the page has; the same shift only spaces it a notch tighter. */
@media (display-mode: standalone) and (max-width: 768px) and (max-height: 700px) {
  .pt-login { padding-top: 0; padding-bottom: 0; }
}

/* A filter an agent's access makes pointless is hidden with [hidden]; .pt-f has a
   display of its own, which would otherwise beat the browser's [hidden] rule. */
.pt-filters .pt-f[hidden] { display: none; }

/* ---------- the loading circle (common.js busy / ring) ----------
   The owner's "loading circle", wherever the portal waits (2026-09-19): ink
   turning on a stone ring. Big and alone in the middle of the screen over a
   light veil while an action waits for its answer (a modal <dialog> of its
   own); small in a place that is loading (a tab, the list, a photo). No
   shadow, no colour; slower, not still, for anyone who asks for less motion. */
.pt-ring { display: inline-block; width: 28px; height: 28px; box-sizing: border-box; border-radius: 50%;
  border: 3px solid var(--soft-stone); border-top-color: var(--ink-black);
  vertical-align: middle; animation: pt-turn 0.8s linear infinite; }
.pt-ring--big { display: block; width: 48px; height: 48px; }
@keyframes pt-turn { to { transform: rotate(360deg); } }
.pt-busy { box-sizing: border-box; width: 48px; height: 48px; padding: 0; margin: auto;
  border: 0; background: transparent; overflow: visible; }
/* The circle is not a control: nothing in it takes a key. Opening it gives it
   focus, and Safari on the iPhone then drew the keyboard's focus outline, a
   square round the circle (owner, 2026-09-19). */
.pt-busy:focus, :where(html[data-key]) .pt-busy:focus-visible { outline: none; }
.pt-busy::backdrop { background: rgba(255, 255, 255, 0.55); }
/* the list, before its first answer */
.pt-loading { display: grid; place-items: center; padding: var(--space-10) 0; }
/* a photo or a plan page on its way into the unit's frame */
.pt-frame.is-loading::before { content: ""; position: absolute; left: 50%; top: 50%; z-index: 1;
  width: 36px; height: 36px; margin: -18px 0 0 -18px; box-sizing: border-box; border-radius: 50%;
  border: 3px solid var(--soft-stone); border-top-color: var(--ink-black); animation: pt-turn 0.8s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .pt-ring, .pt-frame.is-loading::before { animation-duration: 2.4s; }
}

/* ---------- pull down to reload (owner's request, 2026-09-19 and 2026-09-20) ----------
   The circle rides in on a white disc, because a pull starts at the top of the
   page and the top of every page is the ink bar — a bare ring would be half
   invisible there, and the disc keeps it readable if the pull carries it past
   the bar onto the white. It follows the thumb at half its speed (drag: no
   transition, so it tracks rather than trails), fades in over the distance the
   pull has to cover, and turns only once it has been let go and the answer is
   on its way. It is common.js's, so every screen has it. */
.pt-pull { position: fixed; z-index: 90; top: calc(var(--sa-t) + 8px); left: 0; right: 0;
  display: grid; place-items: center; pointer-events: none;
  opacity: 0; transform: translateY(-56px);
  transition: opacity var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard); }
.pt-pull.is-drag { transition: none; }
.pt-pull__disc { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%;
  background: var(--crisp-white); }
.pt-pull .pt-ring { animation: none; border-color: var(--soft-stone); }
.pt-pull.is-ready .pt-ring { border-top-color: var(--ink-black); }
.pt-pull.is-going .pt-ring { border-top-color: var(--ink-black); animation: pt-turn 0.8s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .pt-pull.is-going .pt-ring { animation-duration: 2.4s; }
}
