/* Northora Digital — site styles.
 *
 * Shares Ospry's structure because both are static sites written by hand,
 * and diverges on palette so the two never read as the same company. Ink
 * blue and copper: established trade, not software.
 *
 * The product's argument is that it does not dress estimates up as facts, and
 * the design has to carry that. So: no gradient heroes, no floating glass
 * cards, no invented dashboards. Plain structure, real screenshots, generous
 * space, and numbers only where a number is real.
 *
 * Written as one stylesheet rather than a framework because the whole site is
 * a handful of static pages. A build step would add a step between writing a
 * blog post and it being live, which is the exact friction this replaces.
 */

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body { margin: 0; }
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

/* ---------- tokens ----------
 * Neutrals pulled slightly toward the teal so the greys read as chosen
 * rather than inherited from a reset. */
:root {
  --paper:      #F5F3F0;
  --panel:      #FFFFFF;
  --sunk:       #EBE7E1;
  --ink:        #16191F;
  --soft:       #464C57;
  --muted:      #72798A;
  --rule:       #E2DDD6;
  --rule-firm:  #CDC6BC;
  --teal:       #1E3A5F;
  --teal-deep:  #14293F;
  --teal-wash:  #E6ECF3;
  /* What goes on top of --teal. Per theme, because the accent is dark
     in light mode and pale in dark: a fixed white sat at 2.5:1 on
     every call to action for anybody browsing in dark mode. */
  --on-teal:    #FFFFFF;
  /* A second accent used only to mark what is NOT built. Honest labelling
     earns its own colour; sharing the brand colour would make the
     distinction decorative instead of informative. */
  --clay:       #A85A2A;
  --clay-wash:  #F7EDE5;

  --measure: 68ch;
  --gap: clamp(1rem, 2.5vw, 1.75rem);
  --pad-y: clamp(3rem, 8vw, 6rem);

  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --serif: "Fraunces", Georgia, "Times New Roman", serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #101319; --panel: #191D25; --sunk: #1E232C;
    --ink: #E7EEEC; --soft: #AFBEBB; --muted: #7B8A87;
    --rule: #262C36; --rule-firm: #38404E;
    --teal: #7FA8D8; --teal-deep: #A6C4E6; --teal-wash: #1B2634;
    --on-teal: #0B1622;
    --clay: #E29068; --clay-wash: #33201A;
  }
}

/* ---------- base ---------- */
body {
  background: var(--paper);
  color: var(--ink);
  font: 400 17px/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.wrap { width: min(1120px, 100% - 2.5rem); margin-inline: auto; }
.narrow { width: min(720px, 100% - 2.5rem); margin-inline: auto; }

section { padding-block: var(--pad-y); }

h1, h2, h3 { font-family: var(--serif); font-weight: 400; line-height: 1.15;
             text-wrap: balance; margin: 0 0 0.6em; letter-spacing: -0.01em; }
h1 { font-size: clamp(2.2rem, 5.5vw, 3.6rem); }
h2 { font-size: clamp(1.6rem, 3.6vw, 2.4rem); }
h3 { font-size: clamp(1.15rem, 2vw, 1.4rem); }

p { margin: 0 0 1.15em; max-width: var(--measure); }
.lede { font-size: clamp(1.05rem, 1.8vw, 1.25rem); color: var(--soft); }

.eyebrow {
  font: 600 0.72rem/1 var(--sans);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--teal); margin: 0 0 1rem;
}

/* ---------- header ---------- */
.masthead {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
}
.masthead .wrap {
  display: flex; align-items: center; gap: 1.5rem;
  padding-block: 0.9rem;
}
.brand {
  display: inline-flex; align-items: center; gap: 0.55rem;
  font: 700 1.05rem/1 var(--sans); letter-spacing: -0.02em;
  text-decoration: none; color: var(--ink);
}
.brand .mark {
  width: 26px; height: 26px; border-radius: 7px;
  background: var(--teal); display: grid; place-items: center;
  color: #fff; font-size: 0.8rem; font-weight: 800;
}
.nav { margin-left: auto; display: flex; align-items: center; gap: 1.4rem; }
/* :not(.btn) matters. `.nav a` is a class plus an element, which outranks the
   single class on `.btn-primary`, so without this the nav colour wins and the
   call to action renders dark grey on teal — unreadable, and invisible in a
   quick glance at the page because the button shape still looks right. */
.nav a:not(.btn) {
  font: 500 0.92rem/1 var(--sans); text-decoration: none;
  color: var(--soft); transition: color .15s;
}
.nav a:not(.btn):hover,
.nav a:not(.btn):focus-visible { color: var(--teal); }
@media (max-width: 720px) { .nav .hide-sm { display: none; } }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.72rem 1.25rem; border-radius: 9px;
  font: 600 0.95rem/1 var(--sans); text-decoration: none;
  border: 1px solid transparent; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.btn-primary { background: var(--teal); color: var(--on-teal); }
.btn-primary:hover { background: var(--teal-deep); }
.btn-ghost { border-color: var(--rule-firm); color: var(--ink); }
.btn-ghost:hover { border-color: var(--teal); color: var(--teal); }
:where(a, button):focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; }

/* ---------- hero ---------- */
.hero { padding-block: clamp(3.5rem, 9vw, 7rem) clamp(2.5rem, 6vw, 4rem); }
.hero h1 { max-width: 16ch; }
.hero .lede { max-width: 52ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }
.hero-note { margin-top: 1.1rem; font-size: 0.87rem; color: var(--muted); }

/* ---------- the claim strip ---------- */
.claim {
  border-block: 1px solid var(--rule);
  background: var(--panel);
}
.claim-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.claim-grid p { margin: 0; font-size: 0.95rem; color: var(--soft); }
.claim-grid strong { display: block; color: var(--ink); font-size: 1.02rem;
                     margin-bottom: 0.3rem; }

/* ---------- cards ---------- */
.grid { display: grid; gap: var(--gap); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

.card {
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: 14px; padding: 1.5rem;
}
.card h3 { margin-bottom: 0.45rem; }
.card p { font-size: 0.95rem; color: var(--soft); margin-bottom: 0; }

/* ---------- shots ---------- */
.shot {
  border: 1px solid var(--rule-firm); border-radius: 12px;
  overflow: hidden; background: var(--panel);
  box-shadow: 0 18px 40px -28px rgba(17,24,23,.45);
}
.shot img { width: 100%; }
.shot figcaption {
  padding: 0.85rem 1.1rem; font-size: 0.87rem; color: var(--muted);
  border-top: 1px solid var(--rule);
}
figure { margin: 0; }

/* ---------- proof ---------- */
.proof {
  background: var(--ink); color: #EAF2F0;
  border-radius: 16px; padding: clamp(1.75rem, 4vw, 2.75rem);
}
.proof h2 { color: #fff; }
.proof p { color: #C3D2CF; }
.proof .figure {
  font-family: var(--serif); font-size: clamp(2.4rem, 6vw, 3.6rem);
  color: var(--teal); line-height: 1; margin: 0 0 0.3rem;
}
@media (prefers-color-scheme: dark) {
  .proof { background: var(--sunk); border: 1px solid var(--rule); }
}

/* ---------- honest table ---------- */
.table-scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 0.94rem; }
th, td { text-align: left; padding: 0.8rem 1rem; border-bottom: 1px solid var(--rule); }
th { font: 600 0.78rem/1.3 var(--sans); letter-spacing: 0.06em;
     text-transform: uppercase; color: var(--muted); }
.tag {
  display: inline-block; padding: 0.2rem 0.55rem; border-radius: 999px;
  font: 600 0.72rem/1.4 var(--sans);
}
.tag-live { background: var(--teal-wash); color: var(--teal-deep); }
.tag-soon { background: var(--sunk); color: var(--muted); }
.tag-no   { background: var(--clay-wash); color: var(--clay); }

/* ---------- blog ---------- */
.post-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0; }
.post-list li { border-bottom: 1px solid var(--rule); }
.post-list a {
  display: block; padding: 1.4rem 0; text-decoration: none;
}
.post-list a:hover h3 { color: var(--teal); }
.post-list h3 { margin-bottom: 0.3rem; transition: color .15s; }
.post-list p { margin: 0; font-size: 0.95rem; color: var(--soft); }
.post-meta { font-size: 0.82rem; color: var(--muted); margin-bottom: 0.4rem; }

article.post { padding-block: var(--pad-y); }
article.post h2 { margin-top: 2.2rem; }
article.post ul, article.post ol { max-width: var(--measure); padding-left: 1.2rem; }
article.post li { margin-bottom: 0.5rem; }
article.post blockquote {
  margin: 1.8rem 0; padding: 0.2rem 0 0.2rem 1.2rem;
  border-left: 3px solid var(--teal); color: var(--soft);
  font-size: 1.05rem;
}

/* ---------- cta ---------- */
.cta {
  background: var(--teal-wash); border: 1px solid color-mix(in srgb, var(--teal) 25%, transparent);
  border-radius: 16px; padding: clamp(1.75rem, 4vw, 2.75rem); text-align: center;
}
.cta p { margin-inline: auto; }
.cta .hero-actions { justify-content: center; }

/* ---------- footer ---------- */
.foot {
  border-top: 1px solid var(--rule); padding-block: 2.5rem;
  font-size: 0.9rem; color: var(--muted);
}
.foot .wrap { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: baseline; }
.foot a { color: var(--soft); text-decoration: none; }
.foot a:hover { color: var(--teal); }
.foot nav { margin-left: auto; display: flex; flex-wrap: wrap; gap: 1.2rem; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important;
                           scroll-behavior: auto !important; }
}

/* ===================================================================
   Motion and two-column layouts.

   Everything here is deliberately cheap. This site's whole argument is
   that it builds fast websites for trades, so a heavy hero effect would
   contradict the pitch on the page it decorates. The drifting mist is
   one canvas of soft blobs, it stops when scrolled out of view, and it
   disappears entirely for anyone who asks for reduced motion.
   =================================================================== */

/* ---------- atmospheric header ---------- */
.hero-atmos { position: relative; overflow: hidden; isolation: isolate; }
.hero-atmos > .wrap { position: relative; z-index: 1; }
#mist {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 0; opacity: .55; pointer-events: none;
}
/* A wash under the canvas so the mist reads as weather over the page
   rather than as shapes floating on nothing. */
.hero-atmos::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 55%, var(--paper) 100%);
}

/* ---------- scroll reveal ----------
   Starts visible. If the script never runs — blocked, failed, ancient
   browser — the page still reads normally, which matters more than the
   animation does. */
.reveal { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0; transform: translateY(14px);
    transition: opacity .55s ease, transform .55s ease;
  }
  .js .reveal.seen { opacity: 1; transform: none; }
}

/* ---------- two-column ----------
   The old layout ran text to about half the width and left the rest
   empty, which read as a document rather than a site. */
.split {
  display: grid; gap: clamp(1.5rem, 4vw, 3rem);
  grid-template-columns: 1fr; align-items: center;
}
@media (min-width: 860px) {
  .split { grid-template-columns: 1.05fr 1fr; }
  .split.flip > :first-child { order: 2; }
}
.split p { max-width: 54ch; }

/* ---------- work gallery ---------- */
.work-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.work-item {
  border: 1px solid var(--rule); border-radius: 12px; overflow: hidden;
  background: var(--panel); text-decoration: none; color: inherit;
  transition: transform .25s ease, box-shadow .25s ease, border-color .25s;
}
.work-item:hover, .work-item:focus-visible {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--teal) 40%, transparent);
  box-shadow: 0 16px 34px -22px rgba(22,25,31,.5);
}
.work-item img { aspect-ratio: 4 / 3; object-fit: cover; width: 100%; }
.work-item .meta { padding: .95rem 1.1rem; }
.work-item h3 { font-size: 1.05rem; margin-bottom: .2rem; }
.work-item p { font-size: .88rem; color: var(--muted); margin: 0; }

/* ---------- counters ---------- */
.stat-row {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.stat .n {
  font-family: var(--serif); font-size: clamp(2rem, 4.5vw, 2.8rem);
  color: var(--teal); line-height: 1; display: block;
  font-variant-numeric: tabular-nums;
}
.stat p { font-size: .9rem; color: var(--muted); margin: .35rem 0 0; }

/* ===================================================================
   Craft pass — Northora.

   Same level of finish as Ospry, deliberately different shapes. Ospry
   sells software to agencies and shows a laptop; this sells to trades who
   read their phone in a van, which the homepage copy already says. A
   phone is the honest device here, not a stylistic coin-flip.
   =================================================================== */

/* ---------- display typography ---------- */
h1 { font-variation-settings: "opsz" 120; letter-spacing: -.022em; line-height: 1.08; }
h2 { font-variation-settings: "opsz" 72; letter-spacing: -.016em; }
.lede { text-wrap: pretty; }
.stat .n, table td { font-variant-numeric: tabular-nums; }

/* ---------- interaction ---------- */
.btn {
  position: relative;
  transition: background .18s ease, border-color .18s ease, color .18s ease,
              transform .12s ease, box-shadow .18s ease;
}
.btn-primary {
  box-shadow: 0 1px 2px rgba(22,25,31,.16), 0 6px 14px -8px rgba(30,58,95,.55);
}
.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(22,25,31,.18), 0 12px 22px -10px rgba(30,58,95,.6);
}
.btn-ghost:hover { transform: translateY(-1px); background: var(--panel); }
.btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(22,25,31,.2); }

article.post p a, .split p a, .narrow p a {
  color: var(--teal); text-decoration: none;
  background-image: linear-gradient(var(--teal), var(--teal));
  background-size: 0% 1.5px; background-position: 0 100%;
  background-repeat: no-repeat; transition: background-size .25s ease;
  padding-bottom: 1px;
}
article.post p a:hover, .split p a:hover, .narrow p a:hover { background-size: 100% 1.5px; }

.shot { transition: transform .3s ease, box-shadow .3s ease; }
.shot:hover { transform: translateY(-3px); }

/* Work tiles get a copper hairline on hover — the second accent finally
   doing something, so the palette has range rather than one blue. */
.work-item::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--clay); transform: scaleX(0); transform-origin: left;
  transition: transform .3s ease;
}
.work-item { position: relative; }
.work-item:hover::after { transform: scaleX(1); }

/* ---------- the phone ----------
 * Portrait, held. Angled the other way to Ospry's laptop and cropped by
 * its container, so the two sites never read as the same template. */
.kit { position: relative; overflow: hidden; padding-block: clamp(3rem,8vw,5.5rem); }
.kit-grid { display: grid; gap: clamp(1.5rem,4vw,3rem); grid-template-columns: 1fr;
            align-items: center; }
@media (min-width: 900px) { .kit-grid { grid-template-columns: 1.15fr .85fr; } }

.phone { perspective: 1600px; display: flex; justify-content: center; }
.phone-inner {
  width: min(300px, 78vw);
  transform: rotateY(12deg) rotateX(3deg) rotate(2deg);
  transform-origin: center;
  transition: transform .5s cubic-bezier(.2,.7,.3,1);
}
.phone-inner:hover { transform: rotateY(6deg) rotateX(1deg) rotate(1deg); }
.phone-body {
  background: linear-gradient(155deg,#2B3138 0%,#181D23 55%,#0F1216 100%);
  border-radius: 34px; padding: 10px;
  box-shadow:
    0 2px 3px rgba(22,25,31,.2),
    0 26px 44px -20px rgba(22,25,31,.5),
    0 70px 120px -60px rgba(22,25,31,.6);
}
/* Slot matched to the capture (676x1560) rather than to a spec sheet, so
   nothing is cropped and the mockup shows what a phone actually shows. The
   earlier version forced a landscape screenshot into a portrait slot and
   cover-cropped it to a zoomed slice, which is why it looked wrong. */
.phone-screen {
  position: relative; aspect-ratio: 676 / 1560; border-radius: 26px;
  overflow: hidden; background: var(--panel);
}
.phone-screen img { width: 100%; height: auto; display: block; }
.phone-screen::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(125deg, rgba(255,255,255,.14) 0%,
    rgba(255,255,255,.03) 28%, transparent 55%);
}
.phone-notch {
  position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
  width: 86px; height: 22px; border-radius: 12px; background: #0F1216; z-index: 2;
}

/* ---------- the loud moment, Northora's version ----------
 * Not a dark slab with a huge number — that is Ospry's. This is a light
 * band with an oversized pull quote, which suits a business selling
 * judgement rather than measurement. */
.band {
  background: var(--sunk);
  border-block: 1px solid var(--rule);
  margin-block: clamp(3rem,8vw,5rem);
  padding-block: clamp(3rem,8vw,5rem);
}
.pull {
  font-family: var(--serif);
  font-variation-settings: "opsz" 144;
  font-size: clamp(1.6rem, 4.2vw, 2.9rem);
  line-height: 1.18; letter-spacing: -.02em;
  color: var(--ink); max-width: 22ch; margin: 0;
}
.pull em { font-style: normal; color: var(--clay); }
.band-note { margin-top: 1.4rem; font-size: .95rem; color: var(--muted); max-width: 46ch; }

@media (prefers-reduced-motion: reduce) {
  .btn, .shot, .phone-inner, .work-item::after { transition: none; }
  .btn:hover, .shot:hover { transform: none; }
  .phone-inner:hover { transform: rotateY(12deg) rotateX(3deg) rotate(2deg); }
}

/* ---------- GHL form slot ----------
 * The embed is an iframe, which normally means a third-party request on
 * every page load whether or not anyone scrolls that far. This slot holds
 * the space and only loads the iframe when it comes into view, so the
 * automations behind the form are kept without the page paying for them
 * up front.
 *
 * The reserved height stops the page jumping when it loads, which is both
 * a real annoyance and something Google measures. */
.form-slot {
  position: relative;
  min-height: 610px;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--panel);
  overflow: hidden;
}
.form-slot iframe { width: 100%; height: 100%; min-height: 610px; border: 0; display: block; }
.form-slot .form-ph {
  position: absolute; inset: 0; display: grid; place-content: center;
  gap: .6rem; text-align: center; padding: 2rem; color: var(--muted);
  font-size: .95rem;
}
.form-slot.loaded .form-ph { display: none; }

/* Booking slot. Same lazy behaviour as the form — the widget only loads
   when somebody scrolls to it — but taller, because a calendar needs the
   room and GHL resizes it after load. */
.booking-slot { min-height: 780px; }
.booking-slot iframe { min-height: 780px; }

/* Tap-to-call in the masthead.
   Deliberately not a second button: two buttons of equal weight side by side
   is a choice nobody makes. It reads as a number, behaves as a link, dials on
   a phone.

   Selector is .nav a.phone-link, not .phone-link, because .nav a would
   otherwise win on specificity and repaint it as an ordinary nav item. */
.nav a.phone-link {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--teal);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: .42rem;
}
/* A handset drawn in CSS rather than fetched: it is two shapes, and an extra
   request for an icon on a site that sells fast websites would be poor form.
   aria-hidden by virtue of being generated content, so it is never read out
   in place of the number itself. */
.nav a.phone-link::before {
  content: "";
  width: .95em;
  height: .95em;
  flex: none;
  background: currentColor;
  -webkit-mask: var(--phone-glyph) center / contain no-repeat;
  mask: var(--phone-glyph) center / contain no-repeat;
}
:root {
  --phone-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25 11.4 11.4 0 0 0 3.6.57 1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1a11.4 11.4 0 0 0 .57 3.6a1 1 0 0 1-.25 1z'/%3E%3C/svg%3E");
}
.nav a.phone-link:hover { color: var(--clay); }

/* Social marks in the footer. Sized to sit on the same baseline as the text
   links beside them rather than as a separate row of buttons: they are one
   more way to get in touch, not a call to action competing with the phone
   number. currentColor so they inherit the footer's link colour and its
   hover without a second declaration. */
.foot .social {
  display: inline-flex;
  align-items: center;
  padding: 0 .1rem;
}
.foot .social svg {
  width: 1.05rem;
  height: 1.05rem;
  fill: currentColor;
  display: block;
}
.foot .social:hover svg { fill: var(--clay); }

/* The real brand mark, traced from Greg's logo. Overrides the letterform
   tile that stood in for it: an SVG needs no background, no centring and no
   type styling, and the tile would have boxed the logo into a navy square it
   was never drawn to sit in.

   It keeps the brand teal rather than inheriting the masthead's navy. This is
   the one place the true brand colour appears, which is what a logo is for. */
.brand svg.mark {
  background: none;
  width: auto;
  height: 1.55rem;
  fill: #1BA798;
  display: block;
  flex: none;
}
