/* xp-search.css — experimental UX module (branch experimental/mobile-ux-v2):
   recent searches, saved searches, and the jump bar. Every rule is scoped under
   html.xp so the sheet is completely inert while the flag is off (CLAUDE.md
   "xp" law) — the public archive must render byte-identical to master until the
   owner flips the toggle.
   Palette/type reuse app.css vars (--accent/--dim/--hair/--sunk/etc) so this
   reads as the SAME product, not a bolted-on skin. Labels are authored
   lowercase (some skins set --caps:none, which would otherwise leave whatever
   case we wrote); text-transform still applies on skins that want it. */

/* ═══ ICON BUTTONS (owner request 2026-07-28: "smaller icons instead of text") ═══
   The search bar carried three text labels — "search", "filters" and a glyph — which ate the
   width the INPUT needs on a phone. These collapse to icons.

   Two deliberate choices:
   · The touch target stays 44x44. The ask was "smaller", but the research floor this branch
     is built on is Apple's 44pt / Material's 48dp minimum, and a search control is a primary
     action. Icon-only already reclaims most of the width (a "search" label is ~80px, this is
     44px), so the bar gets much roomier without shrinking the hit area below the floor.
   · The label is NOT deleted — `font-size:0` collapses it visually while leaving the text in
     the accessibility tree, so the button keeps its accessible name with no aria plumbing and
     no markup change. Do not swap this for `text-indent:-9999px` (leaks a huge box) or for
     removing the text node (silences the button for screen readers).
   Icons are CSS masks over `currentColor`, so they inherit the theme in both light and dark
   and need no asset files — this repo has no build step.

   ⚰ THE HEIGHT HALF OF THAT FLOOR IS GONE, AND THAT IS THE OWNER'S SECOND REPORT OF THIS
   BUTTON (forage-h5v, 2026-08-22: *"the search button still fucked up"*). `min-height:44px` on
   the button ITSELF made the search bar's flex line 44px tall while the pill box beside it drew
   40.8 — so the button HUNG 3.2px BELOW the box it belongs to, with their centres 1.6px apart,
   at 390, at 360 and at 1280 with a coarse pointer, which is the screenshot he sent. ⚖ THIS IS
   THE CLOSED ROAD app.css NAMES ON `.btn` IN AS MANY WORDS — *"CLOSED ROAD: min-height:
   var(--tap-min) on `.btn` ITSELF … the floor is a promise about where a THUMB LANDS"* (owner
   ruling 2026-08-20, docs/DECISIONS.md "The floor may not change what a member can see"). This
   rule was written in JULY, before that ruling existed, and is the last copy of the blunt shape
   it closed. ⚠ NOTHING IS LOST: `.btn::after` in app.css already draws the 44px overhang under
   `@media(pointer:coarse)`, and the button is a `.btn`, so the TAP box is still 44px tall while
   the DRAWN box is now exactly the input's. ⚖ THE WIDTH STAYS 44px and is not the same
   question — the overhang is vertical only, because a horizontal halo steals the tap from the
   control beside it, so horizontal floor must be spent as drawn width. */
html.xp .searchbar button[type="submit"] {
  font-size: 0 !important;          /* hide the label, keep it for assistive tech */
  min-width: 44px; width: 44px;
  padding: 0; flex: 0 0 auto;
  position: relative;
}
html.xp .searchbar button[type="submit"]::before {
  content: ""; display: block;
  width: 18px; height: 18px; margin: 0 auto;
  background-color: currentColor;   /* the mask paints in the button's own colour */
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: 18px 18px; mask-size: 18px 18px;
}
/* magnifier */
html.xp .searchbar button[type="submit"]::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E");
}
/* No mask support (old browsers): fall back to the text label rather than a blank button. */
@supports not ((-webkit-mask-image: url("")) or (mask-image: url(""))) {
  html.xp .searchbar button[type="submit"] { font-size: inherit !important; width: auto; padding: 0 10px; }
  html.xp .searchbar button[type="submit"]::before { display: none; }
}


/* ═══ THE FAB — THE SEARCH ENTRY POINT (owner ruling 2026-07-28) ═══
   Bottom-right, thumb reach on a phone (mobile-first law). It used to open a "jump bar" and
   carried a » glyph; it now opens the FULL-PAGE SEARCH CONSOLE, so it wears a magnifier —
   the same CSS-mask-over-currentColor treatment as the submit button above, for the same
   reasons (no asset files, inherits the theme, and `font-size:0` collapses the label
   VISUALLY while leaving "search" in the accessibility tree).

   ⚠ IT IS NO LONGER HIDDEN ON WIDE DESKTOP. It used to `display:none` at min-width:1800px so
   it could not share the corner with app.css's `.rail-slot` dossier. That was safe while the
   search bar was still on the page; it is not safe now that the bar is hidden and this is the
   primary way to search. It shifts LEFT of the rail's 260px track instead. */
/* ⚖ AND IT IS A CELL, NOT A DISC (forage-qw2w.32) — `.mdial-fab` (app-curation.css) carries the
   whole argument and the same eight-gradient bracket construction; these two corners must read
   as ONE control, so a radius on either is a radius on both. 46px here too: the 56px puck was
   the biggest object on a 390px screen and it was the round one. */
html.xp .xps-fab{
  position:fixed; right:max(10px,env(safe-area-inset-right,0px)); z-index:var(--z-fab, 300);
  width:46px; height:46px; border-radius:0;
  border:1px solid var(--accent-deep); background-color:var(--panel); color:var(--accent);
  background-repeat:no-repeat;
  background-image:
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent));
  background-size:9px 1px,1px 9px,9px 1px,1px 9px,9px 1px,1px 9px,9px 1px,1px 9px;
  background-position:
    3px 3px,3px 3px,
    calc(100% - 3px) 3px,calc(100% - 3px) 3px,
    3px calc(100% - 3px),3px calc(100% - 3px),
    calc(100% - 3px) calc(100% - 3px),calc(100% - 3px) calc(100% - 3px);
  font-size:0;                       /* label hidden from sight, kept for assistive tech */
  line-height:1; display:flex; align-items:center; justify-content:center;
  box-shadow:0 8px 24px rgba(0,0,0,.45); cursor:pointer;
  /* ⚠ THE HOME INDICATOR MOVES THE BUTTON, IT DOES NOT PAD IT (fixed 2026-08-09, the day the
     insets stopped being inert). This was `padding-bottom:env(safe-area-inset-bottom)` on a
     56×56 border-box circle that centres its glyph with flex — so the first time env() returned
     a real 34px the content box collapsed to 22px and the magnifier jammed against the top rim
     of its own circle, on the archive's primary way to search. The button has a fixed size; the
     inset belongs to its OFFSET, exactly as .mdial-fab (app-curation.css) already had it. */
  /* ⚖ THE INSET IS SAMPLED ONCE, NOT READ LIVE (2026-08-26). `--sab` is written onto <html> by
     xp-search.js at mount and RE-READ only on a real orientation change; the `env()` below is
     the no-JS fallback and the value the button wears for the frames before the first sample
     lands (the same number, so nothing jumps). WHY IT IS NOT env() DIRECTLY: see the road note
     underneath — a number taken once cannot STEP mid-gesture, whatever the browser reports
     mid-gesture. ⚠ Re-sampling on anything but a rotation puts the live read back. */
  bottom:calc(16px + var(--sab, env(safe-area-inset-bottom, 0px)));

  /* ⛔ CLOSED ROAD: DO NOT SEAT THIS BUTTON FROM `visualViewport` (owner report 2026-08-25:
     *"the search button at the bottom right still jiggles"*, the survivor of the bottom-bar
     pass that landed d11990c). `.arcade-hud` already paid for that road — the whole autopsy
     is in web/common-points.js beside mountArcadeHud(), and the two failure modes it names
     (innerHeight lagging vv.height, and a layout write on every scroll event) apply here
     verbatim. THAT ROAD IS STILL SHUT, and so is the `will-change` below. What is NOT still
     standing is the verdict this block used to end on.

     ⚰ WITHDRAWN 2026-08-26: *"this is the browser dragging its fixed layer, a page cannot opt
     out."* The owner falsified it with two screenshots of the grid whose PAGE CONTENT IS
     PIXEL-IDENTICAL — the same four posts at the same offsets, the same Android navigation bar
     at the same y — and the FAB sitting ~125 device px apart between them. On a 390 CSS px
     viewport rendered 1080 wide that is ≈45 CSS px, and his words were *"it sits in different
     positions based on scroll direction"*: two RESTING positions, not a transient.
     ⚖ IDENTICAL CONTENT RULES THE VIEWPORT OUT. Had the URL bar opened or closed, the content
     would have moved with it. It did not — so the visual viewport is not what changed, and
     Chrome's fixed-layer compensation cannot be the cause. The measurements below were real
     and are kept; the conclusion drawn from them was not earned.

     THE MEASUREMENTS, AND THE LIMIT THAT INVALIDATED THE VERDICT. 8-swipe wiggle on the real
     seeded grid at 390x844 in Chrome-Android URL-bar geometry (a visible 788 inside a layout
     844), 182 animation frames:

       · attribute mutations on the FAB during the gesture ...... 0
         Nothing toggles a class on it, writes its style, or animates it. `touch-action:none`
         is the ONLY inline it ever carries and ModeDial.attach() writes it once, at mount.
         STILL TRUE — and it is why the suspect is the VALUE the rule reads, not any code.
       · its rect in LAYOUT coordinates ......................... ONE distinct value, swing 0px
         True of THAT run, in which the inset was 0 and therefore could not move it. It is not
         evidence about a device where the inset is 48dp.
       · containing block ...................................... the viewport (offsetParent
         null); zero ancestors carry transform/filter/perspective/contain/will-change, so it
         is not being positioned against a box that reflows. STILL TRUE.
       · env(safe-area-inset-bottom) ........................... ONE distinct value all gesture
         ⛔ THE MEASUREMENT THAT PROVED NOTHING, and it said so in its own footnote: headless
         has no navigation bar, so it held at ZERO — and zero cannot step. A constant that is
         constant only because the thing it measures is ABSENT falsifies nothing. The owner's
         phone has a THREE-BUTTON nav bar; ≈45 CSS px is almost exactly Android's 48dp nav-bar
         inset, and on Android Chrome the reported inset can change as the URL bar shows and
         hides — scroll-direction driven, which is exactly what he described.

     ⛔ AND `will-change:transform` IS ALSO CLOSED, because it was TRIED AND MEASURED, not
     reasoned about. The FAB is ALREADY on its own composited layer (reason "Overlap") and was
     painted 0 times in 3s of flinging; adding will-change changed the promotion REASON string
     and nothing else — same 0 paints, identical frame strip. It would have bought a permanent
     layer for zero motion, so it is not here. A `will-change` that buys nothing costs memory.
     ⛔ NOR A TRANSITION, A DEBOUNCE OR A SMOOTHER. Hiding the motion only makes the same jump
     arrive a frame late, and that wrong fix already cost one pass here.

     ✅ WHAT IS DONE INSTEAD is the `--sab` sample above. If the inset steps, a value read once
     cannot follow it; if it does not, the button is seated on exactly the number it had
     before and nothing changed. Either way it stays correct through a rotation, because a
     rotation is the one thing that re-samples it.
     ⚠ ONLY THIS RULE WAS CONVERTED. Two dozen other declarations across web/*.css spend
     env(safe-area-inset-bottom) and every one would step the same way IF this holds — which
     is not yet confirmed on the owner's device, and converting them all on an unconfirmed
     hypothesis is how a fix becomes a regression. One symptom, one change, measurable.
     🔬 HOW IT GETS CONFIRMED: `?sabprobe=1` (xp-search.js) puts the LIVE inset, the sampled
     `--sab`, this button's computed `bottom` and the min/max inset seen since load on screen,
     so one screenshot after a wiggle answers it. `?sabprobe=0` turns it off. If the readout
     shows the inset holding ONE value on his phone too, this hypothesis is dead and both the
     probe and this conversion come out. Rig: scratchpad motion/fab.js. */
}
html.xp .xps-fab::before{
  content:""; display:block;
  width:22px; height:22px;
  background-color:currentColor;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center; mask-position:center;
  -webkit-mask-size:22px 22px; mask-size:22px 22px;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E");
          mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E");
}
html.xp .xps-fab:hover{border-color:var(--accent); color:var(--bright)}
/* no mask support: show the word rather than an empty circle */
@supports not ((-webkit-mask-image: url("")) or (mask-image: url(""))) {
  html.xp .xps-fab{font-size:10px; letter-spacing:calc(.08em * var(--track))}
  html.xp .xps-fab::before{display:none}
}
/* clear app.css's .rail-slot (fixed, right:16, width:260) instead of disappearing */
@media(min-width:1800px){ html.xp .xps-fab{right:292px} }

/* ⚰ THIS GUARDS A TRANSITION THAT DOES NOT EXIST — audited 2026-08-26 across every rule in
   web/*.css and every `style.transition` write in web/*.js: NOTHING declares a transition that
   reaches `.xps-fab`. (`.mdial,.mdial-fab` in app-curation.css is the identical dead pair; the
   two were written together.) It is KEPT rather than deleted, and deliberately: it is a
   standing pre-commitment that the day this button ever animates — a press, a hover, a mode
   badge — a reduced-motion member does not get the animation, and the rule is one line that
   computes to nothing until then. ⛔ It is NOT a licence to add a transition to hide the
   jiggle; that road is shut above. */
@media(prefers-reduced-motion:reduce){ html.xp .xps-fab{transition:none} }

/* ═══ LINK MODE COLLISION (bug found 2026-07-29, part of the "sends me back but nothing
   happens" report) ═══
   Link mode (web/xp-linkmode.css/.js) pins a `+` at the bottom-right of every tile, and this
   FAB is ALSO pinned bottom-right of the viewport — same corner, same rough size, same
   corner (and on overlap the FAB wins the hit test — not because --z-fab outranks the pick's
   3, which it never competed with: the pick lives inside `main.wrap.pagecol`, a z-index:1
   stacking context, so the whole COLUMN ranks 1 against a FAB on <body>). On a grid
   scrolled to its end, the bottom-right tile's + and the FAB occupy the same few square
   millimetres. If that's the tile the owner taps on their phone, the FAB eats the tap and it
   looks exactly like nothing happened.
   Fix: HIDE, not displace. Link mode is a MODE — you are picking a post, not starting a new
   search, so the FAB has no job while it's active, and hiding it needs no second position to
   verify against every viewport width the way a displacement would. `html.lm-on` is set by
   LinkMode.mount()/unmount() (xp-linkmode.js) for exactly this kind of "is a mode active"
   hook, so hiding restores immediately the instant link mode ends — the search entry point is
   never left dead. Deliberately NOT scoped under html.xp: if a future page ever renders the
   FAB without that flag, it must still lose the collision. */
html.lm-on .xps-fab { display: none; }
