/* The three screens of the FinOps web interface.
 *
 * Every colour, spacing step, radius, font stack and shadow is a custom
 * property in the one :root block below. Nothing under that block spells a
 * colour out. The reason is a test: tests/test_web.py reads brand.py, checks
 * the six palette properties against it, and then checks that no hex value
 * appears anywhere else in this file. A component that inlines a colour puts
 * the page and the PDF a refactor apart from disagreeing.
 *
 * The posture is the light one the cloudar-brand skill reserves for
 * content-heavy pages: an off-white ground, black text, orange as an accent on
 * the masthead rule, the table headers and the focus rings. The dark deck
 * posture is for slides.
 *
 * The skill covers PowerPoint, Word and Reveal.js. Buttons, tables, focus
 * states and the breakpoint are derived here.
 */

:root {
  /* --- palette, pinned to src/finops_report/brand.py ------------------- */

  /* The one accent. Everything else in the brand is greyscale. */
  --orange: #FF9900;
  --ink: #000000;
  --muted: #666666;
  --rule: #E7E6E6;
  /* Same value as --rule, and the same reason brand.py gives: the brand has
   * one light grey, so a border drawn over a banded row would be the colour
   * of the row it divides. The tables carry no gridlines at all. ADR 0004. */
  --band: #E7E6E6;

  /* The identity palette this estate is drawn in. No chart lives on these
   * three screens yet. The hues are declared because a chart added later that
   * picked its own would disagree with every PDF in the archive. The order is
   * the one the dataviz validator measured, so it is not to be rearranged.
   * See ADR 0009. */
  --categorical-1: #2a78d6;
  --categorical-2: #eb6834;
  --categorical-3: #1baf7a;
  --categorical-4: #eda100;
  --categorical-5: #e87ba4;
  --categorical-6: #008300;

  /* --- surfaces ------------------------------------------------------- */

  /* Off-white ground with white blocks on it. The brand's white is what a
   * panel and a table row are; the page behind them sits one step down so the
   * blocks have an edge without a border. */
  --page-ground: #FAF9F7;
  --surface: #FFFFFF;

  /* --- typography ----------------------------------------------------- */

  /* Poppins is self-hosted from the @font-face block below. The fallback is a
   * real system stack rather than bare sans-serif, because a reader on a slow
   * connection sees it for the swap period. */
  --font: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --text-xs: 0.75rem;
  --text-s: 0.8125rem;
  --text-m: 0.9375rem;
  --text-l: 1.125rem;
  --text-xl: 1.5rem;
  --leading: 1.5;
  --leading-tight: 1.25;
  --weight-body: 400;
  --weight-strong: 600;

  /* --- spacing, radii, shadow ----------------------------------------- */

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  --radius: 6px;
  --radius-pill: 999px;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 10px rgba(0, 0, 0, 0.04);

  --page-width: 72rem;
  --border: 1px;
  --focus-width: 3px;
  --focus-offset: 2px;
}

/* The faces live in src/finops_report/assets/fonts/ and are reached here at
 * assets/fonts/, which is where #212's BucketDeployment publishes that
 * directory. The repository holds one set of brand assets. A second copy under
 * web/ would be a file that drifts from the one the PDF renders with.
 *
 * Two faces, not five. The SemiBold face claims 600 and 700 so a heading or a
 * <strong> asking for bold resolves to a real face rather than a synthesised
 * one. font-display: swap shows the fallback stack while the file loads. */
@font-face {
  font-family: "Poppins";
  src: url("assets/fonts/Poppins-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Poppins";
  src: url("assets/fonts/Poppins-SemiBold.ttf") format("truetype");
  font-weight: 600 700;
  font-style: normal;
  font-display: swap;
}

/* --- base ------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  background: var(--page-ground);
}

body {
  margin: 0;
  background: var(--page-ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-m);
  font-weight: var(--weight-body);
  line-height: var(--leading);
  -webkit-text-size-adjust: 100%;
}

h1,
h2,
h3 {
  margin: 0 0 var(--space-3);
  font-weight: var(--weight-strong);
  line-height: var(--leading-tight);
}

h1 {
  font-size: var(--text-xl);
}

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

h3 {
  font-size: var(--text-m);
}

p {
  margin: 0 0 var(--space-4);
}

a {
  color: var(--ink);
  text-decoration-color: var(--orange);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

a:hover {
  color: var(--muted);
}

/* The JavaScript toggles the hidden attribute on panels, on the reseller row
 * and on empty states. A table row carries display: table-row from the user
 * agent stylesheet, which outranks the agent's own [hidden] rule once any
 * author rule sets display. The important keeps the attribute winning. */
[hidden] {
  display: none !important;
}

/* --- layout ----------------------------------------------------------- */

/* The outer column. The side padding is the gutter and is set once here, so no
 * component has to remember it and no width loses it. */
.page {
  max-width: var(--page-width);
  margin-inline: auto;
  padding-inline: var(--space-5);
  padding-block: var(--space-5) var(--space-7);
}

/* The logo, the site name, the signed-in reader and the sign-out button. The
 * orange rule under it is the page's one orange divider, which is how the
 * printed report frames a page as well. */
.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
  padding-block: var(--space-3) var(--space-4);
  border-bottom: 2px solid var(--orange);
  margin-bottom: var(--space-5);
}

.masthead img {
  display: block;
  width: auto;
  height: 28px;
}

.masthead h1 {
  margin: 0;
  font-size: var(--text-l);
}

/* Pushed to the trailing edge, and allowed to wrap under the name on a phone
 * rather than squeezing the site name.
 *
 * The block margins are cleared because this is a paragraph, and the base rule
 * gives a paragraph a trailing margin. A flex item is centred on its margin
 * box, so that one-sided margin lifted the reader's address and their scope
 * badge half a step above the site name and the sign-out beside them. The row
 * gap is what spaces this line from the one it wraps under on a phone. */
.masthead__who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-block: 0;
  margin-inline-start: auto;
  color: var(--muted);
  font-size: var(--text-s);
}

.crumbs {
  margin-bottom: var(--space-4);
  font-size: var(--text-s);
}

.crumbs a {
  color: var(--muted);
}

/* --- blocks ----------------------------------------------------------- */

/* The landing sign-in block and every empty state. */
.panel {
  background: var(--surface);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-5);
  margin-bottom: var(--space-5);
}

.panel > :last-child {
  margin-bottom: 0;
}

/* An inline message. The left bar carries the severity, because the brand has
 * no red and inventing one would put a seventh colour on the page. */
.notice {
  border-inline-start: 3px solid var(--rule);
  background: var(--surface);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  font-size: var(--text-s);
  color: var(--muted);
}

.notice--error {
  border-inline-start-color: var(--orange);
  color: var(--ink);
  font-weight: var(--weight-strong);
}

/* --- badges ----------------------------------------------------------- */

.badge {
  display: inline-block;
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  background: var(--band);
  color: var(--ink);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  white-space: nowrap;
}

/* Black on orange rather than the brand's white on orange. At this size white
 * against the orange is a 2.2:1 contrast, which pdf.py rejected for the same
 * reason on its table headers. */
.badge--stale {
  background: var(--orange);
  color: var(--ink);
}

.badge--muted {
  background: transparent;
  border: var(--border) solid var(--rule);
  color: var(--muted);
}

/* --- buttons ---------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: var(--border) solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-s);
  font-weight: var(--weight-strong);
  line-height: var(--leading-tight);
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
  text-decoration: none;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.button:hover {
  background: var(--band);
  color: var(--ink);
}

.button--primary {
  background: var(--orange);
  border-color: var(--orange);
  color: var(--ink);
}

.button--primary:hover {
  background: var(--surface);
  color: var(--ink);
}

.button--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--muted);
  font-weight: var(--weight-body);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  padding-inline: var(--space-2);
}

.button--quiet:hover {
  background: transparent;
  color: var(--ink);
  text-decoration-color: var(--orange);
}

/* A disabled download is the common case on this list, not an edge: a customer
 * can have figures with no report built. It stays legible so the reason beside
 * it reads as an explanation rather than as a fault. */
.button:disabled,
.button[aria-disabled="true"] {
  background: transparent;
  border-color: var(--rule);
  color: var(--muted);
  font-weight: var(--weight-body);
  cursor: not-allowed;
}

.button:disabled:hover,
.button[aria-disabled="true"]:hover {
  background: transparent;
  color: var(--muted);
}

/* --- tables ----------------------------------------------------------- */

.estate,
.history {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-s);
  margin-bottom: var(--space-5);
}

.estate caption,
.history caption {
  text-align: start;
  color: var(--muted);
  font-size: var(--text-s);
  padding-bottom: var(--space-2);
}

/* Orange header, black text, one rule beneath it, and no gridlines anywhere.
 * The row band separates the rows instead. ADR 0004, and the same style
 * pdf.py's ROWS applies to every findings table. */
.estate th,
.history th {
  background: var(--orange);
  color: var(--ink);
  font-weight: var(--weight-strong);
  text-align: start;
  vertical-align: bottom;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-bottom: var(--border) solid var(--rule);
  position: sticky;
  top: 0;
  z-index: 1;
}

.estate td,
.history td {
  padding: var(--space-2) var(--space-3);
  border: 0;
  vertical-align: top;
}

/* The download is a control sitting in a cell beside plain text, and a cell is
 * aligned to its top. Its own block padding put the label seven pixels below
 * the figures in the same row, and made a row carrying a report taller than a
 * row with nothing to download. The cell's padding is the row's rhythm. The
 * inline padding stays, so the hover and focus box still reads as a control. */
.estate td .button,
.history td .button {
  padding-block: 0;
}

.estate tbody tr:nth-child(even),
.history tbody tr:nth-child(even) {
  background: var(--band);
}

/* Hover inverts the band rather than introducing a second light grey. The
 * brand has one, and the band and a border are already the same value. */
.estate tbody tr:hover,
.history tbody tr:hover {
  background: var(--band);
}

.estate tbody tr:nth-child(even):hover,
.history tbody tr:nth-child(even):hover {
  background: var(--surface);
}

/* Figures line up on the decimal point, which is what tabular-nums buys and
 * what money.py's grouped thousands assume a reader can scan down. */
.numeric {
  text-align: end;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The header over a figure column, which needs saying twice. `.estate th` above
 * carries a type selector and outweighs a bare `.numeric`, so without this the
 * two money headings sit left over figures that sit right. */
.estate th.numeric,
.history th.numeric {
  text-align: end;
}

/* A sortable header is a th holding a button. The arrow reads the aria-sort
 * the JavaScript writes, so the indicator cannot drift from what a screen
 * reader is told. */
.sortable {
  cursor: pointer;
}

.sortable button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

th[aria-sort]::after {
  content: "\2195";
  margin-inline-start: var(--space-1);
  color: var(--ink);
  opacity: 0.35;
}

th[aria-sort="ascending"]::after {
  content: "\2191";
  opacity: 1;
}

th[aria-sort="descending"]::after {
  content: "\2193";
  opacity: 1;
}

/* --- row states ------------------------------------------------------- */
/*
 * The estate states are classes on one <tr> rather than one state field,
 * because a row can be stale, unreported and expired at once. They all show. A
 * hidden one would make the list lie about how much of the estate is covered.
 */

/* Nothing collected. The figures are blank, so the row is greyed to say the
 * blank is an absence rather than a zero. */
.is-missing td {
  color: var(--muted);
}

/* Past the staleness threshold the list response carries. The figures still
 * show; the orange edge and the badge beside the age say how old they are. */
.is-stale td:first-child {
  box-shadow: inset 3px 0 0 0 var(--orange);
}

/* Figures with no report built. Only the download is affected, so the row
 * keeps its colour and the disabled button carries the reason. */
.is-unreported .button {
  color: var(--muted);
}

/* A report the lifecycle rule has deleted, which is not the same thing as one
 * that was never built. The disabled download takes a dashed edge, so the two
 * read apart at a glance and not only in the sentence beside them. */
.is-expired .button:disabled {
  border-style: dashed;
  border-color: var(--muted);
}

/* A report older than the figures beside it. The stamp is the one thing that
 * differs from the row above it, so the stamp is what gets weight. */
.is-behind .badge {
  background: transparent;
  border: var(--border) solid var(--orange);
  color: var(--ink);
}

/* A report with no snapshot beside it, on the detail page. The download works,
 * and the Held column is what says which half the archive has. Every Cloudar
 * Reseller Report stamp is one of these: that document is built from no
 * snapshot at all, so nothing here may claim one was held and then deleted. */
.is-reportonly td {
  color: var(--muted);
}

.is-reportonly .button {
  color: var(--ink);
}

/* --- odds and ends ---------------------------------------------------- */

.muted {
  color: var(--muted);
}

.spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.125em;
  border: 2px solid var(--rule);
  border-top-color: var(--orange);
  border-radius: var(--radius-pill);
  animation: spin 700ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* --- focus ------------------------------------------------------------ */

/* Every interactive element takes the same ring, and it is orange so a reader
 * tabbing the estate list can see where they are on a banded row. :focus-visible
 * rather than :focus, so a mouse click on a sort header does not leave one
 * behind. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
  outline: var(--focus-width) solid var(--orange);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius);
}

/* --- one breakpoint --------------------------------------------------- */

/* At 60rem the estate table drops the columns marked .optional rather than
 * scrolling sideways. A table that scrolls sideways on a phone hides the
 * column a reader came for and gives no sign it is there. The gutter stays,
 * narrower. */
@media (max-width: 60rem) {
  .page {
    padding-inline: var(--space-4);
  }

  .optional {
    display: none;
  }

  .masthead__who {
    margin-inline-start: 0;
    width: 100%;
  }

  .estate,
  .history {
    font-size: var(--text-xs);
  }
}

/* --- reduced motion --------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* The spinner stops rather than flickering. It still reads as a ring, and
   * the loading text beside it is what says the page is working. */
  .spinner {
    animation: none;
    border-top-color: var(--orange);
  }
}
