html, body { margin: 0; padding: 0; background: #3778B5; -webkit-font-smoothing: antialiased; }
* { box-sizing: border-box; }
a { color: #FF2E9A; text-decoration: none; }
a:hover { color: #1D3D64; }
input, button, textarea, select { font-family: 'Space Mono', ui-monospace, monospace; }
input:focus { outline: 3px solid #FF2E9A; outline-offset: 2px; }

/* ---------------------------------------------------------------------------
   LIVE RING — a rotating conic-gradient border marking the one control the
   screen most wants clicked next.

   Technique from the ski11s skill "Neumorphic Theme Switcher for Angular 19 —
   Animated Accent Rings, Pill Buttons & Sun/Moon Toggle": accents live in :root
   independent of anything else, and a rAF loop rotates a conic-gradient behind
   a padded wrapper. What is NOT taken is the neumorphic shell — soft pills and
   layered inner shadows belong to that design, not this one. Here the ring
   replaces a hard 3px border on a square button and keeps the drop shadow, so
   the page still reads as the same object, just alive.

   Re-skinning the ring is a five-line edit below; app.js reads these by name.
   --------------------------------------------------------------------------- */
:root {
  --ring-deep:      #12294A;
  --ring-dark:      #1D3D64;   /* the border colour it replaces — the ring's rest state */
  --ring-primary:   #2B5C92;
  --ring-light:     #FF2E9A;   /* the brand magenta: this is the part that reads as motion */
  --ring-highlight: #FFF1F8;
}

.live-ring {
  position: relative;
  padding: 3px;                /* exactly the border width it replaces — no layout shift */
  /* The RESTING look is the border this wrapper stands in for, so a control that is
     not currently being pointed at is pixel-identical to how it always looked. Set
     --ring-rest inline per control (magenta panel, navy buttons). app.js clears the
     inline background when a hint expires, which falls back to exactly this. */
  background: var(--ring-rest, var(--ring-dark));
  /* enumerated, never `all` — `all` animates from browser defaults on mount */
  transition: box-shadow .2s ease;
}
/* Flex, so the button fills the wrapper on BOTH axes. Without it a wrapper that a
   parent flex row stretches taller than its button (Find, beside the taller scope
   caret) shows the ring colour as a bar under the label instead of an even frame. */
.live-ring { display: flex; }
.live-ring > button { flex: 1 1 auto; width: 100%; border: none; }

/* A control that is ring-ABLE but not currently ringed must not animate anything;
   the loop only ever touches [data-ring]. */

/* ---------------------------------------------------------------------------
   The goods & services action rows.

   FIND (beside the query box) and NEXT (beside "what else?") have to line up on
   BOTH edges. Letting each size to its own content cannot do that — the labels
   are different lengths — so the two rows share one grid whose right-hand column
   is a fixed width. Change --gs-action-w and both rows move together.

   The width is chosen so the narrowest phone still fits: at 360px the shell
   leaves 328px, so 104 + 8 gap leaves 216px of query box, and the breakpoint
   below trims it further on anything narrower.
   --------------------------------------------------------------------------- */
.gs-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--gs-action-w, 104px);
  gap: 8px;
  align-items: stretch;
}
.gs-actions > * { min-width: 0; }
@media (max-width: 390px) { .gs-actions { --gs-action-w: 88px; } }

/* ---------------------------------------------------------------------------
   VEIL — "this is still here, it just does not count".
   Laid OVER a card rather than swapping its background, so the card keeps its own
   colours and the veil reads as a state applied to it. pointer-events:none is not
   cosmetic: everything under a veil stays clickable, which is the whole point —
   a mark left out of the score is still a real search result you may want to open.
   --------------------------------------------------------------------------- */
.veiled { position: relative; }
.veiled > .veil {
  position: absolute;
  inset: 0;                    /* the padding box: the card's border stays crisp */
  pointer-events: none;
  background: rgba(143, 160, 181, .34);
}

/* ---------------------------------------------------------------------------
   DOCUMENT PROGRESS MODAL — the screen a paying customer watches for ninety
   seconds while a clearance report is written.

   Only the two pieces that MOVE live here; the modal's colour and weight are
   inline like every other overlay in app.js. The bar's width is never set in
   CSS or in the markup string — app.js paints it from the job row, so the
   transition below is the only thing that interpolates, and it interpolates
   between two values the runner actually reported.
   --------------------------------------------------------------------------- */
.docjob-bar {
  height: 100%;
  width: 0;                      /* painted from state; see paintDocJob() */
  background: #FF2E9A;
  transition: width .45s cubic-bezier(.22, .61, .36, 1);
}
.docjob-spin {
  animation: spin 1s linear infinite;
}

/* Reduced motion: keep the ring, drop the rotation. app.js never starts the
   loop in this case, so nothing overwrites this with an inline background.

   The document modal is held to the same rule, and it costs it nothing: the
   PERCENTAGE and the stage label are the real signal and both are text. Only
   the easing and the spinner go — the bar still jumps to where the job is. */
@media (prefers-reduced-motion: reduce) {
  .live-ring {
    background: linear-gradient(90deg, var(--ring-dark), var(--ring-light), var(--ring-dark));
  }
  .docjob-bar { transition: none; }
  .docjob-spin { animation: none; }
}

/* ---------------------------------------------------------------------------
   THE IN-APP DOCUMENT VIEWER, ON A PHONE.

   These are PAGED documents: sheets laid out for print, 820px wide, with data tables of up to
   eight columns. A 390px viewport can show that in one of three ways and only one of them is
   readable.

     * SCALE the sheet to fit. Proportionally perfect and useless: 820 -> 390 is 0.47, which
       turns the report's 14px Georgia body into 6.6px. A document nobody can read without
       pinching is not a document that has been shown to them.
     * SCROLL the whole sheet sideways. The reader loses the left edge of every line and has to
       pan back for each one.
     * REFLOW, which is what this does — and mostly it is already done. The server's sheets are
       fluid by construction: `max-width:820px` rather than `width`, and `clamp(24px,5vw,56px)`
       padding, both ported from the mock. So the sheet simply becomes the width of the phone
       and the prose re-wraps at that width, exactly as the mock viewer screens do.

   WHAT DOES NOT REFLOW IS A TABLE, and that is the whole of the work below. A LIKENESS / BAND /
   OWNER / STATUS grid has a minimum width no wrapping can get under, so on a narrow screen each
   table becomes its own scroll box: the reader swipes the TABLE sideways and the page around it
   stays put. Nothing else in the viewer may scroll horizontally — see .doc-view's overflow-x.

   Narrow only. On a laptop the tables lay out as tables, because there they fit.
   --------------------------------------------------------------------------- */
.doc-view { overflow-x: hidden; }

/* THE DOCUMENT SUBTREE IS CONTENT-BOX, BECAUSE THAT IS WHAT IT WAS AUTHORED FOR.
   Line 2 of this file sets `* { box-sizing: border-box }`, which is right for the app and wrong
   for the documents: every renderer in app/docs sizes for WeasyPrint, whose default is
   content-box, and certificate.py's seal says so in its own comment -- its ring is width:86px
   plus 11px of padding, meaning 108px. Under border-box that ring's border box IS 86px, its
   108px background is clipped to a radius-43 circle, and the stroke -- which lives between
   radius 43 and 54 -- falls entirely outside the clip. Measured in Chrome: a 1px grey hairline
   with the score floating in it, on screen, while the PDF drew the ring correctly.
   This is the one rule that makes the viewer and the file the same document by construction,
   rather than by two sets of arithmetic that have to be kept in agreement by hand. */
.doc-view .report-page,
.doc-view .report-page * { box-sizing: content-box; }
/* User text with no spaces in it — a 60-character goods statement, a mark like
   XXXXXXXXXXXXXXXXXXXX — would otherwise set the sheet's minimum width and push the page
   sideways however narrow the viewport gets. */
.doc-view .report-page { overflow-wrap: anywhere; }
.doc-view .report-page img { max-width: 100%; height: auto; }

/* THE SCROLLER AND THE TABLE ARE TWO ELEMENTS, BECAUSE ONE ELEMENT CANNOT BE BOTH.

   This block used to say `display:block; overflow-x:auto` on the <table> itself, described as a
   scroll box whose inner table keeps its own min-content width. It is not one. display:block
   re-parents the rows into an anonymous table box sized to the block, so the wider inner table
   the reader is meant to swipe never exists. MEASURED in Chrome at 390px on the served conflict
   list: 59 tables, every one overflow-x:auto, every one scrollWidth == clientWidth == 316px --
   nothing to swipe -- with CLASSES set as C/L/A/S/S/E/S and every BAND value as B/E/L/O/W.

   So app/docs/html.py::tablebox() wraps every table the server emits in .doc-tablebox. The DIV
   scrolls; the TABLE inside it keeps a width worth scrolling to. app.js puts the same wrapper
   round any table that arrives without one -- documents stored before this existed. Measured
   again on the same document afterwards: every box 676px of content in a 316px window, and not
   one run of text laid out over more lines than it has words. */
.doc-view .report-page .doc-tablebox { max-width: 100%; }

/* A TABLE CELL BREAKS AT WORDS, AT EVERY WIDTH. This rule lived inside the phone breakpoint and
   the desktop kept `anywhere`, so a report read on a laptop printed the mark AMAG as AMA / G,
   amure as amu / re, the registration TM1849602 as TM184 / 9602, and the column heading Mark as
   Mar / k. The phone was measured at 390px and fixed; the width where somebody actually reads a
   ten-page report was never re-checked.

   THIS IS NOT COSMETIC ON THIS PRODUCT. A mark name and a registration number are ATOMIC
   IDENTIFIERS -- the reader's next act is to type TM1849602 into the CIPO register, and a number
   split across two lines is one they can transcribe wrong. `anywhere` and `break-word` differ in
   exactly the way that matters here: `anywhere` counts toward intrinsic size, so min-content
   becomes ONE CHARACTER and any column may be squeezed to a single letter; `break-word` breaks a
   word only when that word alone cannot fit its line, which still saves a 200-character goods
   statement and never touches a five-character mark.

   `anywhere` stays on .report-page, where it is load-bearing: it is what lets a sheet carrying an
   unbroken 60-character token reflow to a phone instead of scrolling the page sideways. Only the
   tables are exempted, and only because they have their own scroller to escape into. */
.doc-view .report-page .doc-tablebox > table { overflow-wrap: break-word; }

@media (max-width: 760px) {
  /* -webkit-overflow-scrolling for momentum on iOS, where a nested scroller without it feels
     broken. */
  .doc-view .report-page .doc-tablebox {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }


  /* EXCEPT WHEN THE TABLE IGNORES ITS CONTENT. `table-layout:fixed` -- the conflict list's
     ranked table, which sets nine column widths as percentages precisely so a 45-character mark
     cannot steal width from LIKENESS -- takes no floor from what is in the cells. At the box's
     316px its 5% CLASSES column is 16px wide whatever the wrapping rule says, so it needs the
     width stated. 673px is the printed text column (A4 less 2x16mm, the number paper.py's
     margins are derived from), so what the reader swipes across is the table as it appears in
     the PDF beside them, column for column, and not a third layout nobody has proofread. */
  .doc-view .report-page .doc-tablebox-fixed > table { min-width: 673px; }

  /* A RECORD CARD STACKS ITS LABEL ABOVE ITS VALUE ON A PHONE.

     html.py::records() lays each field out as a flex row: a 116px label, then the value. That is
     right on a 178mm sheet and wrong on a 390px screen, where 116px plus the gap is over a third
     of the line -- taken from the value, and the value is the register sentence that made this a
     card instead of a table in the first place ("Search record: unknown; register check:
     REGISTERED/LIVE (TMA1104694, expiry 2031-11-25)"). Squeezed into 198px that sentence runs to
     nine lines and the card is worse than the table it replaced.

     The label keeps its weight and colour, so it still reads as a label with nothing above it.

     !important, AND IT IS EARNED. The row is laid out by an INLINE `display:flex` on the element
     itself, because the PDF is rendered by WeasyPrint and WeasyPrint never sees this stylesheet
     -- paper.PAGED_MEDIA_CSS and html.PRINT_CSS are the only sheets on that path, so the flex
     has to travel with the markup or the printed card loses its two columns. An inline
     declaration outranks every selector here, so without !important this block computes to
     nothing at all. MEASURED before it was added: in Chrome at 390px the pair still reported
     `display: flex`, which is how this was caught rather than assumed. */
  .doc-view .report-page .doc-pair { display: block !important; }
  .doc-view .report-page .doc-pair > span:first-child { display: block; margin-bottom: 1px; }
}

/* printing the clearance report: only the white pages, one per sheet */
@media print {
  body { background: #fff; }
  body * { visibility: hidden; }
  .report-page, .report-page * { visibility: visible; }
  .report-page { box-shadow: none !important; margin: 0 auto !important; page-break-after: always; }
  .launch-ribbon, .appnav { display: none !important; }
}

/* oblique "newly released" ribbon pinned across the top-right corner. A NOTICE, NOT A LINK:
   it used to be an <a> to trademart.ca "for a demo", and trademart.ca is now the same product,
   so index.html makes it a plain div — no pointer cursor, no hover underline, nothing to click. */
.launch-ribbon {
  position: fixed;
  top: 79px;
  right: -70px;
  width: 300px;
  transform: rotate(45deg);
  z-index: 100;
  background: #FFD535;
  color: #1D3D64 !important;
  border: 2px solid #1D3D64;
  font: 700 11px/1 'Space Mono', ui-monospace, monospace;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-align: center;
  padding: 10px 0;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .25);
  text-decoration: none;
  cursor: default;
  /* IT CANNOT TAKE A PRESS. Pinned across the corner it overlaps the top-right of the shell's
     account button at desktop widths (measured: scripts/qa/ribbon_cover.py). While it was a link
     a press there left the site; as a plain div it would simply have swallowed the press. With
     nothing on it to click, there is no reason for it to be in the way of anything. */
  pointer-events: none;
}
.launch-ribbon { overflow: hidden; white-space: nowrap; }
/* 18s, not the 14s the old wording had: the sentence is longer (98 characters against 76) and
   the track moves one whole copy per cycle, so the same duration would scroll it a third faster
   than anyone reads. Same speed as before, in other words. */
.ribbon-track {
  display: inline-block;
  white-space: nowrap;
  animation: ribbonScroll 18s linear infinite;
}
.ribbon-track > span { display: inline-block; }
/* Pauses under the pointer, which was never about it being a link: it is how anybody reads a
   moving sentence. Only where the ribbon can feel a pointer at all — the flat-strip skins turn
   pointer-events back on, because in the flow it is on top of nothing. */
.launch-ribbon:hover .ribbon-track { animation-play-state: paused; }
@keyframes ribbonScroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* app shell: phone column by default, wide layout on desktop */
.shell, .appnav { width: min(100%, 560px); }
@media (min-width: 900px) {
  .shell, .appnav { width: min(100%, 1000px); }
  .cols2 { display: grid; gap: 10px; grid-template-columns: 1fr 1fr !important; }
  .cols2 > * { margin-bottom: 0 !important; }
  .cols3 { grid-template-columns: repeat(3, 1fr) !important; }
  .login-grid { display: grid; grid-template-columns: 1fr 1.1fr; gap: 44px; align-items: start; }
  .narrow { max-width: 640px; margin: 0 auto; }
  .settings-cols { columns: 2; column-gap: 20px; }
  .settings-cols > div { break-inside: avoid; margin-bottom: 18px; }
}

/* hero collage: clicked piece lifts to the front with a deeper shadow */
img[data-lift] { transition: filter .35s ease; }
img.front-item { filter: drop-shadow(16px 24px 20px rgba(0,0,0,.42)) !important; }

/* extra hero objects: desktop only, until more are authored */
.dsk { display: none; }
@media (min-width: 900px) { .dsk { display: block; } }

@keyframes fa { 0%,100% { transform: translate3d(0,0,0) rotate(var(--r,0deg)); } 50% { transform: translate3d(6px,-22px,0) rotate(calc(var(--r,0deg) + 6deg)); } }
@keyframes fb { 0%,100% { transform: translate3d(0,0,0) rotate(var(--r,0deg)); } 50% { transform: translate3d(-14px,-16px,0) rotate(calc(var(--r,0deg) - 8deg)); } }
@keyframes fc { 0%,100% { transform: translate3d(0,-10px,0) rotate(var(--r,0deg)); } 50% { transform: translate3d(10px,12px,0) rotate(calc(var(--r,0deg) + 4deg)); } }
@keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-14px); } }
@keyframes ladyFloat { 0%,100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-14px); } }
@keyframes screenIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes barber { to { background-position: 64px 0; } }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes spin { to { transform: rotate(360deg); } }

/* ===========================================================================
   TENANT THEMING SEAM

   js/tenant.js stamps data-tenant on <html> from the origin, in <head>, before
   this sheet paints. Everything a second brand is allowed to change is either a
   token below or a string in the tenant record; screens, flows, documents and
   numbers are not brand and live in app.js.

   THE DEFAULT (tmse.ca) IS THE UNPREFIXED SHEET ABOVE. It is deliberately not
   restated under a tenant-scoped selector of its own: a default that is also an
   override is a default that stops being applied the day someone adds a tenant
   and forgets a rule. tmse.ca renders byte-identically to before this block existed.

   THE SKIN IS A SECOND, SEPARATE SEAM. js/theme.js stamps data-theme on <html> the
   same way, from an admin setting rather than the hostname. THIS SHEET IS "PINK FUN"
   and stays unprefixed for the same reason as above; the other skin lives entirely in
   css/theme-clean-hub.css under html[data-theme="clean-hub"].
   =========================================================================== */

/* A first-time visitor has no cached theme, so theme.js holds first paint until the API
   says which skin — it sets this attribute and ALWAYS clears it, on the answer or after
   ~1.2s, whichever is first. visibility rather than display so app.js lays out and
   measures normally underneath. Set only by script: with JS off it never exists. */
html[data-theme-pending] body { visibility: hidden; }

/* THE RIBBON IS NO LONGER HIDDEN ON trademart.ca. It was, while it was a link sending tmse.ca's
   visitors to trademart.ca "for a demo" — there it would have pointed at itself. It is now a
   plain "newly released" notice that is equally true of both sites, so both show it. */

/* ---------------------------------------------------------------------------
   BRAND NOT SUPPLIED — what a tenant with `placeholder: true` renders as.

   NO TENANT USES THIS TODAY. trademart.ca did until the owner settled that it wears
   the current skin; its record in js/tenant.js is now filled in. The block stays
   because the next brand will arrive before its design does too, and the reasoning
   has not changed: guessing produces something that looks finished, gets
   screenshotted, and becomes the design by default. So this does the opposite: it
   strips the brand layer off and says so, loudly, on every screen.

   WHAT IT REMOVES is exactly the brand layer and nothing else — display faces,
   drop shadows, the hero lettering's stroke. The layout, the flows and every
   figure stay put, because that is the product and it is the same product.

   WHAT IT DOES NOT DO is filter/grayscale the shell. app.js has nineteen
   position:fixed overlays and a comment at gsOverlaysHTML() recording that a
   transformed ancestor hijacks position:fixed away from the viewport — filter
   creates a containing block the same way, so every modal would break.
   --------------------------------------------------------------------------- */
html[data-brand-placeholder], html[data-brand-placeholder] body { background: #6B7280 !important; }

/* System faces everywhere: the Archivo Black / Lobster Two lockup IS the tmse brand,
   and wearing it under another name is the guess this block exists to avoid. */
html[data-brand-placeholder] #root, html[data-brand-placeholder] #root * {
  font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif !important;
  letter-spacing: normal !important;
  -webkit-text-stroke: 0 !important;
  text-shadow: none !important;
  box-shadow: none !important;
}

/* No ribbon on an undesigned brand: it is yellow-and-navy tmse lettering, which is the brand
   layer this block strips, and the hazard banner below already owns the top of the screen. */
html[data-brand-placeholder] .launch-ribbon { display: none !important; }

/* Unmissable, undismissable, and above the z-index:340 modal layer on purpose: this
   must be in every screenshot anyone takes of this origin. */
html[data-brand-placeholder] body { padding-top: 34px; }
html[data-brand-placeholder] body::before {
  content: 'PLACEHOLDER BRAND — NO VISUAL IDENTITY HAS BEEN SUPPLIED FOR THIS DOMAIN. COLOURS BELOW ARE TM SEARCH ENGINE\'S.';
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 9999;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 10px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  background: repeating-linear-gradient(45deg, #111 0 12px, #FFD535 12px 24px);
  color: #fff;
  -webkit-text-stroke: 3px #111;
  paint-order: stroke fill;
  font: 700 11px/34px ui-monospace, monospace;
  text-align: center;
}
@media print { html[data-brand-placeholder] body::before { display: none; } }
