/* =========================================================
   SIGNET — Resources / blog
   ========================================================= */

const POSTS = [
  {
    id: 1, topic: "Security", date: "May 12, 2026", read: "8 min",
    title: "Zero account takeovers: how Signet's Conditional Access policies are rewriting the rules on M365 security.",
    excerpt: "Our standard Conditional Access baseline blocks the attack paths attackers actually use on Microsoft 365 \u2014 and our clients haven't had a single account takeover since we rolled it out.",
    featured: true,
    image: "assets/m365-conditional-access.png",
    author: "Signet",
    body: [
      { kind: "lede", text: "Microsoft 365 is the most attacked productivity platform in the world. Across every client we've taken over from another MSP in the last three years, the same gap shows up: MFA was enabled, but Conditional Access was either off, mis-scoped, or carrying a list of \u201ctrusted\u201d exceptions long enough to make the whole policy ornamental." },
      { kind: "p", text: "This post is the baseline we deploy on day one for every Signet client. It is opinionated. It assumes Microsoft 365 Business Premium or Entra ID P1 at minimum. It also assumes you would rather inconvenience a vendor for thirty seconds than spend a weekend rebuilding from a compromised global admin account." },
      { kind: "h2", text: "What attackers actually do on M365" },
      { kind: "p", text: "The dominant attack pattern in 2025\u201326 is not exotic. It is adversary-in-the-middle phishing \u2014 a lookalike Microsoft login page that quietly relays the user's credentials and MFA approval, then steals the resulting session token. Once the token is on the attacker's machine, MFA is irrelevant. They are signed in as the user." },
      { kind: "p", text: "From there, the playbook is depressingly consistent: silent inbox rules that hide replies from the security team, a payroll redirect or wire fraud email to finance, OAuth consent grants to a malicious app for persistence, and a quick sweep of SharePoint for anything labelled \u2018contracts,\u2019 \u2018banking,\u2019 or \u2018HR.\u2019" },
      { kind: "h2", text: "The Signet Conditional Access baseline" },
      { kind: "p", text: "We deploy nine policies on day one. None of them are clever. All of them block at least one real attack we have seen this year." },
      { kind: "list", items: [
        "Block legacy authentication everywhere \u2014 no exceptions, no \u2018we still need IMAP for the scanner.\u2019 The scanner gets an app password on a service account, or it gets replaced.",
        "Require phishing-resistant MFA for all administrators. FIDO2 keys or Windows Hello for Business; no SMS, no voice, no \u2018approve\u2019 push that a user will tap reflexively at 11pm.",
        "Require compliant or hybrid-joined devices for access to email, SharePoint, and Teams. If the device is not enrolled in Intune and meeting policy, it cannot reach company data \u2014 even with valid credentials and MFA.",
        "Block sign-ins from outside the countries the business actually operates in. We start with an allow-list of one to three countries and add as needed. Travel exceptions go through a named approval, not a permanent rule.",
        "Require MFA for every guest and external user, on every sign-in. Guests inherit your tenant's risk; treat them accordingly.",
        "Block or step-up high-risk sign-ins using Entra ID Protection. Confirmed-compromised users are blocked until a helpdesk-driven password reset and device check.",
        "Require app protection policies for mobile access to Outlook, Teams, and OneDrive. Corporate data is contained; a wiped phone does not take the business with it.",
        "Block unmanaged OAuth consent. Users cannot grant a third-party app access to their mailbox or files without an admin reviewing the request first.",
        "Enforce sign-in frequency and persistent browser sessions appropriately. Stolen tokens have a short shelf life when sessions are bounded.",
      ]},
      { kind: "h2", text: "What this has cost our clients" },
      { kind: "p", text: "Honestly, very little. The first two weeks generate the most helpdesk traffic \u2014 mostly the legacy auth block catching a forgotten line-of-business app, or an unenrolled personal laptop hitting the compliant-device requirement. We resolve those by enrolling the device, replacing the auth method, or granting a narrow, time-boxed exception with a named owner and an end date in the calendar." },
      { kind: "p", text: "After the first month, the policies fade into the background. Nobody talks about Conditional Access on a quiet Tuesday. That is the point." },
      { kind: "h2", text: "What this has prevented" },
      { kind: "p", text: "Across the 240+ tenants we manage, we have not had a single successful account takeover since we standardized this baseline. We have blocked dozens of attempts \u2014 mostly AiTM kits from known phishing infrastructure, a handful of session-token replays from residential proxies, and one extremely persistent attacker who eventually moved on to easier targets." },
      { kind: "p", text: "\u2018Easier targets\u2019 is the real measure of a security program. You will never make yourself unhackable. You can make yourself the second-most-attractive target on the attacker's list, every single day. Conditional Access, deployed properly, does that for Microsoft 365." },
      { kind: "h2", text: "If you want to talk" },
      { kind: "p", text: "We are happy to walk through your current policies on a no-cost call. Bring your tenant, an admin who can read the Entra portal, and 45 minutes. We will tell you what we would change and in what order \u2014 whether you hire us or not." },
    ],
  },
  {
    id: 2, topic: "Operations", date: "Apr 28, 2026", read: "5 min",
    title: "Don't be the first in line: why Signet delays Windows updates by two weeks.",
    excerpt: "Microsoft ships, the internet stress-tests it, then we deploy. A short defense of intentionally being a little behind the latest patch.",
    author: "Signet",
    image: "assets/windows-updates-delay.png",
    body: [
      { kind: "lede", text: "Patch Tuesday is not a deadline. It is a release, and like every release, the first week is when the world finds the bugs. Our standard policy is to hold Windows feature and quality updates for fourteen days behind general availability \u2014 and only then ship them to client fleets." },
      { kind: "h2", text: "What we are protecting against" },
      { kind: "p", text: "Every recent year has produced at least one Patch Tuesday that broke something material: VPN clients, print spoolers, Outlook search, domain controllers, Wi-Fi adapters. The bugs are usually fixed within a week or two, often in an out-of-band release. The clients who hold for fourteen days almost always skip the damage entirely." },
      { kind: "h2", text: "How the two-week ring works" },
      { kind: "list", items: [
        "Day 0 (Patch Tuesday): updates released by Microsoft. Our internal lab and pilot ring (Signet staff devices) take them immediately.",
        "Days 1\u201310: we monitor Microsoft's Known Issues feed, vendor advisories, and the Windows Release Health dashboard. If something breaks, it usually breaks publicly within a week.",
        "Day 14: updates roll out to client production rings in waves, starting with a small pilot group per tenant.",
        "Day 21: full deployment complete, with reporting on success and failure rates by device class.",
      ]},
      { kind: "h2", text: "When we break the rule" },
      { kind: "p", text: "Zero-day fixes for actively exploited vulnerabilities ship immediately, on a separate emergency ring. The two-week hold is for quality and feature updates \u2014 not for security patches against threats already in the wild. We tell clients which is which." },
      { kind: "h2", text: "Why this is not 'being behind'" },
      { kind: "p", text: "Being behind would be running Windows 10 in 2026. Being deliberate is delaying a single biweekly cycle so your finance team isn't on hold with the helpdesk because the new servicing stack update broke their VPN. Predictability beats novelty." },
    ],
  },
  {
    id: 3, topic: "Artificial Intelligence", date: "Apr 14, 2026", read: "6 min",
    title: "How businesses can use AI without exposing confidential data.",
    excerpt: "AI is now in every workflow \u2014 whether you sanctioned it or not. The question isn't whether your team uses it; it's whether they're pasting confidential data into a chatbot you don't control. Here is how to say yes safely.",
    author: "Signet",
    image: "assets/ai-confidential-data.png",
    body: [
      { kind: "lede", text: "Banning AI doesn't work. Your team is already using it \u2014 on their phones, in personal browser tabs, on Tuesday afternoons when a deadline is bigger than the policy. The realistic goal isn't to keep AI out; it is to give your team a safer place to use it than the free public chatbot they will reach for otherwise." },
      { kind: "h2", text: "Why this matters now" },
      { kind: "p", text: "Most AI mistakes are not malicious. Someone pastes a client contract into a free LLM to summarize it. Someone drops a customer list into a chatbot to draft an email campaign. Someone uploads a spreadsheet of PHI to a coding assistant to ask why a formula is broken. Once that data leaves your tenant, you no longer control where it is logged, who trains on it, or how long it sticks around." },
      { kind: "h2", text: "The four levers we use" },
      { kind: "list", items: [
        "Sanction a safe option. Deploy a business-tier AI tool (Microsoft 365 Copilot, ChatGPT Enterprise, or similar) where your tenant owns the data boundary, training is disabled by default, and prompts and outputs stay inside your security perimeter.",
        "Block the unsafe options. Use DNS filtering, app control, and Conditional Access to block consumer AI services on managed devices. Make the sanctioned tool the path of least resistance.",
        "Classify and label your sensitive data. Microsoft Purview (or equivalent) sensitivity labels let you mark documents as Confidential or Restricted and have the AI tool refuse to surface them in untrusted contexts.",
        "Train people on what AI is and isn't safe for. A 20-minute session covering 'never paste anything you wouldn't email to a stranger' goes further than a 40-page policy nobody reads.",
      ]},
      { kind: "h2", text: "What good looks like in practice" },
      { kind: "list", items: [
        "Your team has one sanctioned AI tool, tied to corporate identity, with a clear data boundary you can point to.",
        "That tool is on every managed device, in every browser, and trained on \u2014 only when explicitly approved \u2014 your own internal documents through a governed connector.",
        "Consumer AI services are blocked on managed devices, with a friendly redirect message pointing users to the sanctioned tool.",
        "Sensitive documents carry sensitivity labels that travel with the file and constrain how AI can use them.",
        "Audit logs show who used AI on what, retained long enough to investigate if something goes wrong.",
      ]},
      { kind: "h2", text: "What we never recommend" },
      { kind: "p", text: "We never recommend a blanket AI ban. It fails immediately, hides usage from your security team, and pushes the most useful tool of the decade into the shadow IT pile. We also never recommend pasting source code, customer data, or PHI into a free consumer AI tool \u2014 not even once, not even to 'just test something.'" },
      { kind: "h2", text: "Where Signet helps" },
      { kind: "p", text: "For our clients, we deploy and govern the sanctioned AI stack, set up the data classification and labeling, block the consumer services on managed devices, and provide a short, sharp training session your whole company can sit through in one Tuesday lunch. If you want to look at this seriously, we are happy to walk through what your team is using today and where the actual risk is." },
    ],
  },
  {
    id: 4, topic: "Operations", date: "Mar 30, 2026", read: "12 min",
    title: "Tabletop exercises that don't feel like a waste of time.",
    excerpt: "A three-act format we've run with 40+ clients. Scripts, role assignments, and the questions that produce real change.",
    author: "Signet",
    image: "assets/tabletop-exercises.png",
    body: [
      { kind: "lede", text: "Most tabletop exercises fail the same way: a generic ransomware scenario, a vague 'who would you call,' and a closing slide that recommends improving 'communication.' We have a format that produces actual decisions and actual artifacts." },
      { kind: "h2", text: "Act one: the inciting incident" },
      { kind: "p", text: "Ninety minutes, the executive team in a room, no laptops open. We present a tailored scenario based on the client's actual stack, vendors, and customer base \u2014 not a stock 'malware in the marketing department' story. The first decision they make sets up everything that follows." },
      { kind: "h2", text: "Act two: the second-day problem" },
      { kind: "p", text: "By day two, the news has leaked, an employee has talked to a reporter, and an attorney is asking who authorized the initial communication. Now we are not testing your incident response \u2014 we are testing your reputation, your legal posture, and your willingness to say 'we don't know yet' on the record." },
      { kind: "h2", text: "Act three: the post-mortem" },
      { kind: "p", text: "We end with the question nobody ever asks in a tabletop: 'What would you change tomorrow morning, knowing what you know now?' That is the only question that produces real change. We capture the answers and turn them into a thirty-day action list with named owners." },
      { kind: "h2", text: "What we measure" },
      { kind: "p", text: "Time to first decision. Time to first external communication. Number of 'I assumed' statements logged. Number of action items closed in the following quarter. The last one is the only one that matters." },
    ],
  },
  {
    id: 5, topic: "Networking", date: "Mar 18, 2026", read: "7 min",
    title: "Why a VPN is a must when you travel for work.",
    excerpt: "Hotel Wi-Fi, conference networks, and the coffee shop next to the venue \u2014 all places we have watched attackers harvest credentials in real time. Here is what we install on every traveling laptop.",
    author: "Signet",
    image: "assets/vpn-travel.png",
    body: [
      { kind: "lede", text: "You can do almost everything right on your home and office networks and still hand an attacker your session token from the lobby of a conference hotel. Public and semi-public Wi-Fi is where most of the credential theft we investigate actually starts \u2014 not in inboxes. A correctly configured VPN closes that gap." },
      { kind: "h2", text: "What is actually unsafe out there" },
      { kind: "p", text: "Modern web traffic is encrypted, which protects most of what you do. The risks that remain are real and underrated: rogue access points impersonating the conference SSID, captive portals that prompt for corporate credentials, DNS hijacks pointing your laptop at lookalike login pages, and old/unpatched IoT devices on the same network probing yours." },
      { kind: "h2", text: "What a VPN actually does" },
      { kind: "list", items: [
        "Encrypts every packet between your laptop and a trusted egress point \u2014 not just your browser tabs, but the OS background services, telemetry, and any line-of-business client that doesn't enforce TLS itself.",
        "Hides DNS lookups from whoever runs the local network, so a hijacked DNS server can't redirect you to a phishing page.",
        "Keeps you off the local network as a peer \u2014 the printer in the conference room can't scan your laptop's open ports because it never sees your real address.",
        "Gives you a consistent egress IP that your Conditional Access policies recognize, so location-based rules don't get confused by every airport you connect from.",
      ]},
      { kind: "h2", text: "The Signet travel build" },
      { kind: "p", text: "Every Signet-managed laptop ships with an always-on VPN profile, deployed via Intune, that auto-connects when the device joins any network that is not the user's home or office. The tunnel is split where it should be split (Microsoft 365 traffic goes direct), and full-tunnel everywhere else. The user doesn't toggle anything; the policy enforces itself." },
      { kind: "h2", text: "Habits that pair with the VPN" },
      { kind: "list", items: [
        "Never join an unfamiliar SSID without confirming the exact name with venue staff. 'Marriott_Guest_Free' is not the same network as 'Marriott_Guest'.",
        "Decline captive portals that ask for an email or 'corporate domain' to grant access \u2014 use the cellular hotspot until you can verify.",
        "Keep Bluetooth and AirDrop off in transit. They are not VPN territory, but they are the second-most-common gap we see at airports.",
        "Treat hotel business-center computers as compromised by default. Print boarding passes from your phone.",
      ]},
      { kind: "h2", text: "Bottom line" },
      { kind: "p", text: "A VPN is not a paranoid extra; it is the cheapest single control you can give a traveling employee. We have not had a single account takeover on a road trip in two years \u2014 and it is not because we hired more careful travelers. It is because the laptop refuses to send credentials over a network it doesn't trust, even when the user, busy and tired, would have." },
    ],
  },
  {
    id: 6, topic: "Compliance", date: "Mar 04, 2026", read: "6 min",
    title: "HIPAA compliance isn't optional: here's what your business needs to know.",
    excerpt: "If your business creates, receives, stores, or transmits protected health information, HIPAA applies to you \u2014 whether you are a clinic, a vendor, or the IT firm supporting one. A practical primer for leadership teams.",
    author: "Signet",
    image: "assets/hipaa-compliance.png",
    body: [
      { kind: "lede", text: "HIPAA is not a checkbox. It is a federal standard with real penalties, real enforcement, and a list of recent settlements long enough to fill an afternoon. If your business touches protected health information in any capacity, the question is not whether HIPAA applies \u2014 it is how prepared you are when the Office for Civil Rights or your largest healthcare client asks for proof." },
      { kind: "h2", text: "Who HIPAA actually covers" },
      { kind: "p", text: "Two categories: covered entities (health plans, healthcare clearinghouses, and providers who transmit health information electronically) and business associates (anyone who creates, receives, maintains, or transmits PHI on behalf of a covered entity). If you are an IT provider, an accountant, a marketing agency, or a software vendor serving healthcare clients, you are almost certainly a business associate \u2014 and you need a signed Business Associate Agreement on file before you touch a single record." },
      { kind: "h2", text: "The three rules you have to know" },
      { kind: "list", items: [
        "Privacy Rule: who can see PHI, under what conditions, and what patients are entitled to know about how their information is used.",
        "Security Rule: the administrative, physical, and technical safeguards required to protect electronic PHI \u2014 risk analyses, access controls, audit logs, encryption, contingency plans.",
        "Breach Notification Rule: what counts as a breach, who you have to tell, and how quickly. The 60-day clock starts the day you discover it \u2014 not the day you finish investigating.",
      ]},
      { kind: "h2", text: "What real compliance looks like" },
      { kind: "list", items: [
        "A current, documented risk analysis \u2014 reviewed at least annually and any time your systems materially change.",
        "Written policies and procedures covering every safeguard required by the Security Rule, signed by leadership and trained against.",
        "Encryption of PHI at rest and in transit. Encryption is not strictly required, but in practice it is the only defensible answer when something goes wrong.",
        "Multi-factor authentication for every account that can touch PHI. SMS is no longer considered adequate by current guidance.",
        "Backups that are tested, off-network, and immutable. Ransomware events involving PHI are reportable breaches by default unless you can demonstrate the data was inaccessible to the attacker.",
        "A Business Associate Agreement signed with every vendor who touches PHI \u2014 cloud providers, EHRs, fax services, billing platforms, IT support, all of them.",
        "Audit logs you can actually produce on demand, retained for at least six years.",
      ]},
      { kind: "h2", text: "What it costs to get this wrong" },
      { kind: "p", text: "Civil penalties range from roughly $137 per violation for unknowing failures up to $2.1 million per category per year for willful neglect that is not corrected. Recent settlements have ranged from a few tens of thousands of dollars for small practices to multiple millions for systemic failures. That is before you add the cost of breach notification, credit monitoring, remediation, and the patient calls." },
      { kind: "h2", text: "How Signet approaches it" },
      { kind: "p", text: "For covered-entity and business-associate clients, we run an annual HIPAA risk analysis using the OCR's audit protocol as the template, maintain the policy library, deploy the technical safeguards (encryption, MFA, EDR, immutable backup, segmentation), train staff on Privacy and Security Rule obligations, and keep evidence collection painless so the next audit isn't a fire drill." },
      { kind: "p", text: "If you are not sure where you stand, a one-hour conversation will tell you. We will walk through the seven items above against your current setup and tell you, honestly, which ones are solid and which ones would not survive an audit." },
    ],
  },
];

const ArticleReader = ({ post, onClose }) => ReactDOM.createPortal(
  <div
    onClick={onClose}
    style={{
      position: "fixed", top: 0, left: 0, right: 0, bottom: 0, zIndex: 200,
      background: "rgba(3, 6, 16, 0.82)",
      backdropFilter: "blur(10px)",
      WebkitBackdropFilter: "blur(10px)",
      overflowY: "scroll",
      WebkitOverflowScrolling: "touch",
      overscrollBehavior: "contain",
      animation: "fadeIn 240ms ease",
    }}
  >
    <button
      onClick={onClose}
      aria-label="Close article"
      style={{
        position: "fixed", top: 24, right: 24, zIndex: 210,
        width: 44, height: 44, borderRadius: 999,
        border: "1px solid var(--edge-strong)",
        background: "rgba(8, 12, 28, 0.85)",
        backdropFilter: "blur(6px)",
        WebkitBackdropFilter: "blur(6px)",
        cursor: "pointer", fontSize: 24, lineHeight: 1,
        color: "var(--ivory)",
        fontFamily: "var(--serif)",
      }}
    >
      ×
    </button>
    <div style={{ padding: "48px 24px" }}>
      <article
        onClick={(e) => e.stopPropagation()}
        style={{
          maxWidth: 780, margin: "0 auto",
          background: "var(--surface)",
          color: "var(--ivory)",
          borderRadius: 18,
          overflow: "hidden",
          boxShadow: "0 40px 120px rgba(0, 0, 0, 0.6)",
          border: "1px solid var(--edge)",
        }}
      >
        {post.image && (
          <div style={{ position: "relative", width: "100%", aspectRatio: "16 / 9", overflow: "hidden", background: "#0a1430" }}>
            <img
              src={post.image}
              alt={post.title}
              style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }}
            />
          </div>
        )}
        <div style={{ padding: "56px 64px 72px" }}>
          <div className="blog-meta" style={{ marginBottom: 16 }}>
            <span>{post.topic}</span>
            <span>·</span>
            <span>{post.date}</span>
            <span>·</span>
            <span>{post.read} read</span>
          </div>
          <h1 style={{ fontFamily: "var(--serif)", fontSize: 40, lineHeight: 1.18, letterSpacing: "-0.02em", fontWeight: 400, color: "var(--ivory)", margin: 0 }}>
            {post.title}
          </h1>
          {post.author && (
            <div style={{ marginTop: 24, fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--gold)" }}>
              By {post.author}
            </div>
          )}
          <div style={{ marginTop: 36, display: "flex", flexDirection: "column", gap: 22 }}>
            {post.body.map((b, i) => {
              if (b.kind === "lede") {
                return (
                  <p key={i} style={{ fontFamily: "var(--serif)", fontSize: 22, lineHeight: 1.5, color: "var(--ivory)", fontStyle: "italic", margin: 0 }}>
                    {b.text}
                  </p>
                );
              }
              if (b.kind === "h2") {
                return (
                  <h2 key={i} style={{ fontFamily: "var(--serif)", fontSize: 26, fontWeight: 400, letterSpacing: "-0.015em", color: "var(--ivory)", marginTop: 14, marginBottom: 0 }}>
                    {b.text}
                  </h2>
                );
              }
              if (b.kind === "list") {
                return (
                  <ol key={i} style={{ paddingLeft: 0, margin: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 14 }}>
                    {b.items.map((it, j) => (
                      <li key={j} style={{ display: "grid", gridTemplateColumns: "36px 1fr", gap: 12, alignItems: "baseline", fontSize: 17, lineHeight: 1.55, color: "var(--ivory-soft)" }}>
                        <span style={{ fontFamily: "var(--mono)", fontSize: 12, color: "var(--gold)", letterSpacing: "0.06em" }}>
                          {String(j + 1).padStart(2, "0")}
                        </span>
                        <span>{it}</span>
                      </li>
                    ))}
                  </ol>
                );
              }
              return (
                <p key={i} style={{ fontSize: 17, lineHeight: 1.7, color: "var(--ivory-soft)", margin: 0 }}>
                  {b.text}
                </p>
              );
            })}
          </div>
        </div>
      </article>
    </div>
  </div>,
  document.body
);

const Blog = ({ go }) => {
  const [topic, setTopic] = React.useState("All");
  const [reading, setReading] = React.useState(null);
  const topics = ["All", ...new Set(POSTS.map((p) => p.topic))];
  const visible = topic === "All" ? POSTS : POSTS.filter((p) => p.topic === topic);
  const featured = visible.find((p) => p.featured) || visible[0];
  const rest = visible.filter((p) => p.id !== featured?.id);

  React.useEffect(() => {
    if (!reading) return;
    const onKey = (e) => { if (e.key === "Escape") setReading(null); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [reading]);

  return (
    <main className="page-fade">
      <section className="page-head">
        <div className="shell">
          <span className="eyebrow">Resources</span>
          <h1>Things we think you should know.</h1>
          <p>
            Field notes from the engineers doing the actual work. No fluff,
            no filler. We post when we have something worth saying.
          </p>
          <div className="topics" style={{ marginTop: 32 }}>
            {topics.map((t) => (
              <span
                key={t}
                className={`topic ${topic === t ? "active" : ""}`}
                onClick={() => setTopic(t)}
              >
                {t}
              </span>
            ))}
          </div>
        </div>
      </section>

      {featured && (
        <section className="tight">
          <div className="shell">
            <span className="eyebrow">Featured</span>
            <div className="blog-feature">
              <div className="case-img">
                {featured.image ? (
                  <img
                    src={featured.image}
                    alt={featured.title}
                    style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }}
                  />
                ) : (
                  <Placeholder label="FEATURE — editorial illustration" />
                )}
              </div>
              <div>
                <div className="blog-meta">
                  <span>{featured.topic}</span>
                  <span>·</span>
                  <span>{featured.date}</span>
                  <span>·</span>
                  <span>{featured.read}</span>
                </div>
                <h2>{featured.title}</h2>
                <p>{featured.excerpt}</p>
                <div style={{ marginTop: 32 }}>
                  <button className="btn btn-ghost" onClick={() => featured.body && setReading(featured)}>
                    Read the piece <Arrow />
                  </button>
                </div>
              </div>
            </div>
          </div>
        </section>
      )}

      <section>
        <div className="shell">
          <div className="section-header">
            <div>
              <span className="eyebrow">More writing</span>
              <h2 style={{ marginTop: 16 }}>From the rest of the practice.</h2>
            </div>
          </div>
          <div className="blog-grid">
            {rest.map((p) => (
              <div
                key={p.id}
                className="post"
                onClick={() => p.body && setReading(p)}
                style={{ cursor: p.body ? "pointer" : "default" }}
              >
                <div className="post-img">
                  {p.image ? (
                    <img
                      src={p.image}
                      alt={p.title}
                      style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }}
                    />
                  ) : (
                    <Placeholder />
                  )}
                </div>
                <div className="blog-meta" style={{ marginBottom: 8 }}>
                  <span>{p.topic}</span>
                  <span>·</span>
                  <span>{p.read}</span>
                </div>
                <h3>{p.title}</h3>
                <p className="post-excerpt">{p.excerpt}</p>
                {p.body && (
                  <div style={{ marginTop: 18, fontFamily: "var(--mono)", fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--gold)" }}>
                    Read the piece →
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>
      </section>

      {/* SUBSCRIBE */}
      <section>
        <div className="shell">
          <div style={{ background: "var(--ink)", color: "var(--paper)", borderRadius: 20, padding: 64, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 60, alignItems: "center" }}>
            <div>
              <span className="eyebrow" style={{ color: "var(--gold-soft)" }}>The Signet dispatch</span>
              <h2 style={{ marginTop: 16, color: "var(--paper)" }}>One letter, once a month.</h2>
              <p style={{ marginTop: 16, color: "rgba(245, 240, 230, 0.78)", maxWidth: 440 }}>
                A monthly note from our practice leads — current threats,
                vendor news worth paying attention to, and the occasional
                rant. ~2,300 readers.
              </p>
            </div>
            <form onSubmit={(e) => { e.preventDefault(); alert("Subscribed — check your inbox."); }}>
              <div style={{ display: "flex", gap: 8 }}>
                <input
                  type="email"
                  placeholder="Work email"
                  required
                  style={{
                    flex: 1, padding: "14px 18px", borderRadius: 999,
                    background: "rgba(245, 240, 230, 0.08)",
                    border: "1px solid rgba(245, 240, 230, 0.2)",
                    color: "var(--paper)", fontSize: 15, fontFamily: "inherit",
                  }}
                />
                <button type="submit" className="btn" style={{ background: "var(--gold)", color: "var(--paper)" }}>
                  Subscribe <Arrow />
                </button>
              </div>
              <p style={{ marginTop: 14, fontSize: 12, color: "rgba(245, 240, 230, 0.55)" }}>
                No marketing tricks. Unsubscribe in one click.
              </p>
            </form>
          </div>
        </div>
      </section>
      {reading && <ArticleReader post={reading} onClose={() => setReading(null)} />}
    </main>
  );
};

window.Blog = Blog;
