How do I make an inline heading that's also sticky when immediately followed by a subheading?

I’ve used #box to create inline/run-in headings (see below), but doing so orphans the heading if it’s at the end of the page and doesn’t have inline content (which in my use case only happens when there’s a subordinate heading).

#show heading.where(level: 2): it => {
  box(it) + h(1em)
}

= H1

== H2
#lorem(600)

== H2 // this one is orphaned at the bottom of the page

=== H3
#lorem(10)

Maybe there’s a different way to do inline headings that doesn’t have this side-effect, but I haven’t been able to figure it out. It seems like being sticky requires being a block, but being inline requires not being a block. (Manually inserting page breaks or the like isn’t a good solution for me because my use case is a template for long union contracts, which undergo a ton of edits in the course of negotiations.) Any assistance would be much appreciated!

I’m pretty sure you can’t, because that’s 2 opposite things. block.sticky is only for block heading while run-in heading is inline, hence without block.sticky. So you have to get creative with some show rules.

One way, though at first kinda makes sense, in practice was very tricky. Yet for this example it kinda works in the end.

#import "@preview/scaffolder:0.2.1": get-page-margins

// #show heading.where(level: 2): it => it.body + h(1em, weak: true)
#show heading.where(level: 2): it => {
  let marked = state("marked", ())
  let next-h3 = query(heading.where(level: 3).after(here()))
  // On the same page one right under the other.
  let h3-same = next-h3.filter(h => (
    h.location().page() == here().page()
      and h.location().position().y - here().position().y < 1cm
  ))
  // On the adjacent pages one right under the other.
  let h3-diff = next-h3.filter(h => (
    h.location().page() - here().page() == 1
      and h.location().position().y - get-page-margins().top < 1cm
      and page.height - here().position().y - get-page-margins().bottom < 1cm
  ))
  if h3-diff.len() != 0 or h3-same.len() != 0 { return it }
  it.body + h(1em, weak: true)
}

= H1
== H2.1
#lorem(600)
== H2.2 // this one is orphaned at the bottom of the page
=== H3
#lorem(10)

The second one would be an “everything” show rule instead of heading one. It will probably be even more hacky and messy, but in theory should be more reliable, because you won’t rely on approximate distance and re-layout shenanigans, and instead use in-syntax-tree adjacency. Here is the (almost) bear-bone idea:

#let run-in-heading(it) = it.body + h(1em, weak: true)

#show: doc => context {
  let space = [ ].func()
  let adjacent-h2-index
  let h2-indeces = ()
  let are-sticky = ()
  for (i, child) in doc.children.enumerate() {
    if child.func() == heading {
      let level = child.depth + child.at("offset", default: heading.offset)
      if level == 2 { h2-indeces.push(i) }
      if level == 3 and adjacent-h2-index != none {
        are-sticky.push(adjacent-h2-index)
      }
      adjacent-h2-index = if level == 2 { i } else { none }
    } else if child.func() not in (space, parbreak) {
      adjacent-h2-index = none
    }
  }
  let children = doc.children
  // Or do the opposite with a heading show rule:
  // for i in are-sticky { children.at(i) = block(sticky: true, children.at(i)) }
  for i in h2-indeces.filter(x => x not in are-sticky) {
    children.at(i) = run-in-heading(children.at(i))
  }
  children.join()
}

= H1
== H2.1
#lorem(600)
== H2.2 // this one is orphaned at the bottom of the page
=== H3
#lorem(10)